Skip to content

Commit d3050f7

Browse files
authored
refactor: semantic function clustering improvements (Finding 1–4) (#8507)
Addresses all 4 actionable findings from the automated semantic analysis of 146 non-test Go source files ([#8499](#8499)). ## Finding 1 — `PositiveInteger` delegates to `TimeoutMinimum` `internal/config/validation_rules.go` — eliminates duplicated `< 1` guard; matches the pattern already used by `TimeoutPositive`. Error `Message` and `Suggestion` text preserved via override: ```go // Before: independent < 1 guard if value < 1 { return &ValidationError{...} } // After: delegates, then customises the returned error if err := TimeoutMinimum(value, 1, fieldName, jsonPath); err != nil { err.Message = fmt.Sprintf("%s must be a positive integer (>= 1), got %d", ...) err.Suggestion = fmt.Sprintf("Use a positive integer (>= 1) for %s", ...) return err } ``` ## Finding 2 — `internal/strutil` → `internal/util` Package renamed to `util`: the package contains duration formatting, cryptographic randomness, JSON deep-clone, and collection utilities — none of which are string-specific. All ~26 import sites updated (production, tests, AGENTS.md, CONTRIBUTING.md). ## Finding 3 — Logger factory functions inlined `file_logger.go`, `markdown_logger.go`, `jsonl_logger.go`, `tools_logger.go`, `observed_url_domains_logger.go` — 10 named functions (`setupX` / `handleXError`, 2 per logger × 5 loggers) that were each referenced exactly once as struct field values are inlined as anonymous functions directly in their `loggerFactory` var declarations. Tests updated to use `factory.setup(...)` / `factory.onError(...)`. ## Finding 4 — Route arg-key constants in `proxy/router.go` Seven constants (`argOwner`, `argRepo`, `argPullNumber`, `argIssueNumber`, `argMethod`, `argResourceID`, `argDiscussionNumber`) replace ~20 scattered bare string literals across the route table and `MatchRoute`, providing a single source of truth and typo protection.
2 parents 6a8b060 + de8f9a5 commit d3050f7

46 files changed

Lines changed: 252 additions & 267 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
4545
- `internal/proxy/` - Filtering HTTP proxy for the GitHub API with DIFC enforcement
4646
- `internal/sanitize/` - Sensitive data redaction utilities (`SanitizeString`, `SanitizeJSON`, `TruncateSecret`) for safe log output
4747
- `internal/server/` - HTTP server (routed/unified modes)
48-
- `internal/strutil/` - String and formatting utilities
48+
- `internal/util/` - String, formatting, randomness, and JSON deep-clone utilities
4949
- `internal/syncutil/` - Concurrency utilities
5050
- `internal/sys/` - System utilities
5151
- `internal/testutil/` - Test utilities and helpers

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ gh-aw-mcpg/
296296
├── proxy/ # HTTP forward proxy for DIFC filtering
297297
├── sanitize/ # Sensitive data redaction utilities for logging
298298
├── server/ # HTTP server (routed/unified modes)
299-
├── strutil/ # String and formatting utility helpers (deduplication, trimming, duration formatting, JSON deep-clone)
299+
├── util/ # String, formatting, randomness, and JSON deep-clone utilities
300300
├── syncutil/ # Concurrency utility helpers
301301
├── sys/ # System utilities
302302
├── testutil/ # Test utilities and helpers
@@ -325,7 +325,7 @@ gh-aw-mcpg/
325325
- **`internal/proxy/`** - HTTP forward proxy applying DIFC filtering to `gh` CLI and REST/GraphQL requests
326326
- **`internal/sanitize/`** - Sensitive data redaction utilities (`SanitizeString`, `SanitizeJSON`, `TruncateSecret`) for safe log output
327327
- **`internal/server/`** - HTTP server with routed and unified modes
328-
- **`internal/strutil/`** - String and formatting utility helpers (deduplication, trimming, duration formatting, JSON deep-clone)
328+
- **`internal/util/`** - String, formatting, randomness, and JSON deep-clone utilities
329329
- **`internal/syncutil/`** - Concurrency utility helpers (get-or-create pattern)
330330
- **`internal/sys/`** - System utilities
331331
- **`internal/testutil/`** - Test utilities and helpers

internal/auth/header.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import (
4141

4242
"github.com/github/gh-aw-mcpg/internal/logger"
4343
"github.com/github/gh-aw-mcpg/internal/sanitize"
44-
"github.com/github/gh-aw-mcpg/internal/strutil"
44+
"github.com/github/gh-aw-mcpg/internal/util"
4545
)
4646

4747
var log = logger.New("auth:header")
@@ -206,7 +206,7 @@ func IsMalformedHeader(header string) bool {
206206
// if none is provided. Returns a 32-byte hex-encoded string (64 chars).
207207
func GenerateRandomAgentID() (string, error) {
208208
logAPIKey.Print("Generating random agent ID")
209-
key, err := strutil.RandomHex(32)
209+
key, err := util.RandomHex(32)
210210
if err != nil {
211211
logAPIKey.Printf("Random agent ID generation failed: %v", err)
212212
return "", fmt.Errorf("failed to generate random agent ID: %w", err)

internal/config/guard_policy_validation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"sort"
77
"strings"
88

9-
"github.com/github/gh-aw-mcpg/internal/strutil"
9+
"github.com/github/gh-aw-mcpg/internal/util"
1010
)
1111

1212
const errMsgPolicyMissingKey = "policy must include allow-only or write-sink"
@@ -188,7 +188,7 @@ func NormalizeGuardPolicy(policy *GuardPolicy) (*NormalizedGuardPolicy, error) {
188188
return normalized, nil
189189

190190
case []string:
191-
scopes, err := normalizeAndValidateScopeArray(strutil.StringsToAny(scope))
191+
scopes, err := normalizeAndValidateScopeArray(util.StringsToAny(scope))
192192
if err != nil {
193193
return nil, err
194194
}

internal/config/validation_rules.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ func TimeoutPositive(timeout int, fieldName, jsonPath string) *ValidationError {
4646

4747
// PositiveInteger validates that a value is at least 1.
4848
// Returns nil if valid, *ValidationError if invalid.
49+
// It delegates to TimeoutMinimum with min=1, then overrides Message and
50+
// Suggestion to use "positive integer" phrasing instead of the generic
51+
// "must be at least N" wording produced by TimeoutMinimum.
4952
func PositiveInteger(value int, fieldName, jsonPath string) *ValidationError {
5053
logValidation.Printf("Validating positive integer: field=%s, value=%d, jsonPath=%s", fieldName, value, jsonPath)
51-
if value < 1 {
52-
return newValidationError(
53-
fmt.Sprintf("Positive integer validation failed: %s=%d is not positive", fieldName, value),
54-
fieldName,
55-
fmt.Sprintf("%s must be a positive integer (>= 1), got %d", fieldName, value),
56-
jsonPath,
57-
fmt.Sprintf("Use a positive integer (>= 1) for %s", fieldName),
58-
)
54+
if err := TimeoutMinimum(value, 1, fieldName, jsonPath); err != nil {
55+
logValidation.Printf("Positive integer validation failed: %s=%d is not positive", fieldName, value)
56+
err.Message = fmt.Sprintf("%s must be a positive integer (>= 1), got %d", fieldName, value)
57+
err.Suggestion = fmt.Sprintf("Use a positive integer (>= 1) for %s", fieldName)
58+
return err
5959
}
6060
return nil
6161
}

internal/difc/sink_server_ids.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"sync"
77

88
"github.com/github/gh-aw-mcpg/internal/logger"
9-
"github.com/github/gh-aw-mcpg/internal/strutil"
9+
"github.com/github/gh-aw-mcpg/internal/util"
1010
)
1111

1212
var logSink = logger.New("difc:sink_server_ids")
@@ -31,7 +31,7 @@ func SetSinkServerIDs(serverIDs []string) {
3131
return
3232
}
3333

34-
normalized := strutil.DeduplicateStrings(serverIDs, true)
34+
normalized := util.DeduplicateStrings(serverIDs, true)
3535
if len(normalized) < len(serverIDs) {
3636
logSink.Printf("Removed %d duplicate or empty sink server IDs", len(serverIDs)-len(normalized))
3737
}
@@ -82,7 +82,7 @@ func ParseSinkServerIDs(input string) ([]string, error) {
8282
validated = append(validated, value)
8383
}
8484

85-
result := strutil.DeduplicateStrings(validated, false)
85+
result := util.DeduplicateStrings(validated, false)
8686
logSink.Printf("Parsed %d unique DIFC sink server IDs: %v", len(result), result)
8787
return result, nil
8888
}

internal/githubhttp/collaborator.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/github/gh-aw-mcpg/internal/httputil"
1010

1111
"github.com/github/gh-aw-mcpg/internal/logger"
12-
"github.com/github/gh-aw-mcpg/internal/strutil"
12+
"github.com/github/gh-aw-mcpg/internal/util"
1313
)
1414

1515
var logCollab = logger.New("githubhttp:collaborator")
@@ -19,9 +19,9 @@ var logCollab = logger.New("githubhttp:collaborator")
1919
// It returns the (possibly partial) values even on error so that callers can
2020
// include them in diagnostic log messages.
2121
func ParseCollaboratorPermissionArgs(argsMap map[string]interface{}) (owner, repo, username string, err error) {
22-
owner = strutil.GetStringFromMap(argsMap, "owner")
23-
repo = strutil.GetStringFromMap(argsMap, "repo")
24-
username = strutil.GetStringFromMap(argsMap, "username")
22+
owner = util.GetStringFromMap(argsMap, "owner")
23+
repo = util.GetStringFromMap(argsMap, "repo")
24+
username = util.GetStringFromMap(argsMap, "username")
2525
if owner == "" || repo == "" || username == "" {
2626
logCollab.Printf("ParseCollaboratorPermissionArgs: missing required fields: owner=%q, repo=%q, username=%q", owner, repo, username)
2727
err = fmt.Errorf("get_collaborator_permission: missing owner/repo/username")

internal/guard/wasm_payload.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strings"
77

88
"github.com/github/gh-aw-mcpg/internal/config"
9-
"github.com/github/gh-aw-mcpg/internal/strutil"
9+
"github.com/github/gh-aw-mcpg/internal/util"
1010
)
1111

1212
// normalizePolicyPayload coerces a policy value to a map[string]interface{}.
@@ -251,7 +251,7 @@ func BuildLabelAgentPayload(policy interface{}, trustedBots []string, trustedUse
251251

252252
if len(trustedBots) > 0 {
253253
// trusted-bots is a top-level key in the label_agent payload.
254-
bots := strutil.StringsToAny(trustedBots)
254+
bots := util.StringsToAny(trustedBots)
255255
payload["trusted-bots"] = bots
256256
logWasm.Printf("BuildLabelAgentPayload: injected %d trusted-bots into payload", len(trustedBots))
257257
}
@@ -260,7 +260,7 @@ func BuildLabelAgentPayload(policy interface{}, trustedBots []string, trustedUse
260260
// trusted-users is injected inside the allow-only object.
261261
// If allow-only is absent, the injection is skipped and buildStrictLabelAgentPayload
262262
// will reject the payload when called with the missing allow-only key.
263-
users := strutil.StringsToAny(trustedUsers)
263+
users := util.StringsToAny(trustedUsers)
264264
// Inject into allow-only object if present
265265
if allowOnly, ok := payload["allow-only"].(map[string]interface{}); ok {
266266
allowOnly["trusted-users"] = users

internal/logger/fallback_handlers_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"github.com/stretchr/testify/require"
99
)
1010

11-
func TestHandleFileLoggerError_UsesFallback(t *testing.T) {
11+
func TestFileLoggerFactory_OnError_UsesFallback(t *testing.T) {
1212
var logger *FileLogger
1313
var err error
1414
_ = captureStdLog(t, func() {
15-
logger, err = handleFileLoggerError(assert.AnError, "/tmp/logs", "test.log")
15+
logger, err = fileLoggerFactory.onError(assert.AnError, "/tmp/logs", "test.log")
1616
})
1717
require.NoError(t, err)
1818
require.NotNil(t, logger)
@@ -22,11 +22,11 @@ func TestHandleFileLoggerError_UsesFallback(t *testing.T) {
2222
assert.Equal(t, os.Stderr, logger.logger.Writer())
2323
}
2424

25-
func TestHandleMarkdownLoggerError_UsesFallback(t *testing.T) {
25+
func TestMarkdownLoggerFactory_OnError_UsesFallback(t *testing.T) {
2626
var logger *MarkdownLogger
2727
var err error
2828
_ = captureStdLog(t, func() {
29-
logger, err = handleMarkdownLoggerError(assert.AnError, "/tmp/logs", "test.md")
29+
logger, err = markdownLoggerFactory.onError(assert.AnError, "/tmp/logs", "test.md")
3030
})
3131
require.NoError(t, err)
3232
require.NotNil(t, logger)
@@ -35,11 +35,11 @@ func TestHandleMarkdownLoggerError_UsesFallback(t *testing.T) {
3535
assert.False(t, logger.initialized)
3636
}
3737

38-
func TestHandleToolsLoggerError_UsesFallback(t *testing.T) {
38+
func TestToolsLoggerFactory_OnError_UsesFallback(t *testing.T) {
3939
var logger *ToolsLogger
4040
var err error
4141
_ = captureStdLog(t, func() {
42-
logger, err = handleToolsLoggerError(assert.AnError, "/tmp/logs", "tools.json")
42+
logger, err = toolsLoggerFactory.onError(assert.AnError, "/tmp/logs", "tools.json")
4343
})
4444
require.NoError(t, err)
4545
require.NotNil(t, logger)

internal/logger/file_logger.go

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,28 @@ var (
2323
globalLoggerMu sync.RWMutex
2424
)
2525

26-
// setupFileLogger configures a FileLogger after the log file has been opened.
27-
func setupFileLogger(file *os.File, logDir, fileName string) (*FileLogger, error) {
28-
fl := &FileLogger{
29-
logDir: logDir,
30-
fileName: fileName,
31-
logFile: file,
32-
logger: log.New(file, "", 0),
33-
}
34-
log.Printf("Logging to file: %s", filepath.Join(logDir, fileName))
35-
return fl, nil
36-
}
37-
38-
// handleFileLoggerError falls back to stderr when the log file cannot be opened.
39-
// Stderr is used (not stdout) to avoid corrupting the stdout JSON channel that
40-
// callers use to receive the gateway configuration output.
41-
func handleFileLoggerError(err error, logDir, fileName string) (*FileLogger, error) {
42-
return fallbackLoggerOnInitError(err, "Failed to initialize log file", "Falling back to stderr for logging", &FileLogger{
43-
logDir: logDir,
44-
fileName: fileName,
45-
useFallback: true,
46-
logger: log.New(os.Stderr, "", 0),
47-
})
48-
}
49-
5026
// fileLoggerFactory bundles the setup and error-handler for FileLogger.
5127
var fileLoggerFactory = loggerFactory[*FileLogger]{
52-
setup: setupFileLogger,
53-
onError: handleFileLoggerError,
28+
setup: func(file *os.File, logDir, fileName string) (*FileLogger, error) {
29+
fl := &FileLogger{
30+
logDir: logDir,
31+
fileName: fileName,
32+
logFile: file,
33+
logger: log.New(file, "", 0),
34+
}
35+
log.Printf("Logging to file: %s", filepath.Join(logDir, fileName))
36+
return fl, nil
37+
},
38+
onError: func(err error, logDir, fileName string) (*FileLogger, error) {
39+
// Stderr is used (not stdout) to avoid corrupting the stdout JSON channel that
40+
// callers use to receive the gateway configuration output.
41+
return fallbackLoggerOnInitError(err, "Failed to initialize log file", "Falling back to stderr for logging", &FileLogger{
42+
logDir: logDir,
43+
fileName: fileName,
44+
useFallback: true,
45+
logger: log.New(os.Stderr, "", 0),
46+
})
47+
},
5448
}
5549

5650
// InitFileLogger initializes the global file logger

0 commit comments

Comments
 (0)