From bd6c500040b89a9b4d6185d9310a502a381fe1ee Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 13:25:27 +0900 Subject: [PATCH 1/6] chore(deps): bump declscope to 0.13.0 Reinstall the declscope-adoption skill from the 0.13.0 binary, which now teaches declscope shrink. The analyzer reports nothing new on this tree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KtUXCXiWE99YCb9ZcLmSsn --- .claude/skills/declscope-adoption/SKILL.md | 78 +++++++++++++++++++--- mise.toml | 2 +- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/.claude/skills/declscope-adoption/SKILL.md b/.claude/skills/declscope-adoption/SKILL.md index a0da4f8..80800bd 100644 --- a/.claude/skills/declscope-adoption/SKILL.md +++ b/.claude/skills/declscope-adoption/SKILL.md @@ -3,14 +3,14 @@ name: declscope-adoption description: Adopt declscope on an existing Go codebase and drive its diagnostics to zero. Read this when introducing declscope to a repository, when clearing a declscope baseline, or when a declscope diagnostic is hard to act on. Covers reading the diagnostics as structure, the remedy for each shape, and the measurement traps that produce false confidence. license: MIT x-embedded-by: declscope -x-embedded-version: 0.12.0 -x-embedded-at: "2026-09-24T07:04:57Z" -x-embedded-digest: "sha256:3ff360b2bf9880e00a36ba22f0de09062eb5545f8fccf2a48874b717712d6f58" +x-embedded-version: 0.13.0 +x-embedded-at: "2026-09-25T04:22:46Z" +x-embedded-digest: "sha256:f344c4e28af4379d944b3b6563f74727999fae4218804bb2bf0b57fa31d53093" --- # Adopting declscope -Written against **declscope 0.12.0**. Check the version first: this describes how that release behaves, not how an older one does. +Written against **declscope 0.13.0**. Check the version first: this describes how that release behaves, not how an older one does. ```bash declscope -V=full @@ -102,6 +102,60 @@ done Every count in the rest of this skill assumes `qualify: ondemand` with `exported: true`. That is what the numbers were taken under, not a recommendation. +## Shrink the exported surface first + +**`declscope shrink` reports the exported declarations of `internal/` packages that nothing outside their package uses.** With `-fix` it unexports them. The analyzer cannot answer this: it reads one package, and any importer might use an exported name. Inside `internal/`, Go limits the importers to one directory tree, so `shrink` loads the whole module and sees every one of them. + +An exported name inside `internal/` claims that another package depends on it. Where nothing does, the claim is false, and it hides the declaration from the rest of declscope. An exported declaration takes package scope by default, so no boundary is ever reported on it. Unexported, it takes `private`, and the analyzer checks who reaches it. + +**It is a subcommand, not a rule the analyzer runs.** `go vet` and golangci-lint never report it, and it has no config key. A clean `declscope ./...` says nothing about it. + +### Run it before the analyzer + +**Run `shrink`, and apply its fixes, before you work on the analyzer's reports.** A declaration it unexports becomes private to its namespace. Wherever another file of the package uses it, the analyzer then reports a boundary crossing that was not there before. Fixing in the other order means a second round. + +This happened when declscope held itself to `shrink`. The fix unexported twelve declarations, and nine of them were used from other files of their package. The analyzer then needed nine `//declscope:package` directives to state those crossings. + +| Step | Command | +| --- | --- | +| 1. Read what `shrink` reports | `declscope shrink ./...` | +| 2. Unexport, once the owner agrees | `declscope shrink -fix ./...` | +| 3. Confirm the build | `go vet ./...` and `go test ./...` | +| 4. Read what the analyzer now reports | `declscope ./...` | +| 5. State or move each new crossing | See [What each shape means](#what-each-shape-means) | + +Ask before step 2, the same as any other change. The fix renames every identifier naming the declaration, all inside its own package, and the doc comment that opens with the name. + +### Reading what it reports + +| Report | What to do | +| --- | --- | +| `... uses it` and nothing more | The fix is offered. Apply it with `-fix` | +| `... (no fix: )` | A use may exist that `shrink` cannot prove, or the rename is unsafe. **Do not unexport it by hand.** Read the reason first | +| `... only the external tests of use it` | Keep it exported. Add `//declscope:ignore overexported // ` when the tests use it on purpose | +| `declscope shrink: not judged: : ` on stderr | That package was not checked. It is not clean | + +**A package not judged is not a package with nothing to report.** + +`shrink` stands down wherever an importer could be unseen. That is outside `internal/`, in `package main`, and beside assembly or cgo. It is also under an `internal/` that a nested module's path extends. The stderr line names each such package, and the exit status ignores it. + +Silence a report with `//declscope:ignore overexported` and a reason. A bare `//declscope:ignore` does not reach this rule. `shrink` reports an ignore that silenced nothing, as the analyzer does for its own. + +**Deleting unused code is not `shrink`'s job.** Once a declaration is unexported, staticcheck's `unused` and gopls' `unusedfunc` report it when nothing uses it. Run them after `shrink`, not before. + +### Keep it in CI + +Run it before the analyzer there too, so that a failure reads in the order it is fixed. It exits 3 when it reports anything. + +```yaml +- run: declscope shrink ./... +- run: declscope ./... +``` + +**Names written as strings are outside what `shrink` can see.** + +A template can name a field, and a constant can go to `reflect.Value.MethodByName`. A script can read the symbol table. Each uses a declaration by name. When the value reaches them through an interface, `shrink` already treats it as used. When it does not, add the ignore with the reason. + ## The two kinds of report declscope reports two things. **Read them separately.** @@ -110,6 +164,7 @@ declscope reports two things. **Read them separately.** | --- | --- | | `boundary` | A file reaches a declaration another file holds. A property of the code | | `qualify` | A name does not carry its file's namespace. A convention | +| `overexported` | An exported name inside `internal/` that nothing outside its package uses. Only `declscope shrink` reports it, and it goes [first](#shrink-the-exported-surface-first) | Boundary first. It is the one that points at structure. @@ -309,6 +364,8 @@ go build ./... && declscope ./... # never read a bare count without this **A zero may be the filter, not the code.** A `filter.only` anywhere in the chain can leave a package with nothing to read. A package nothing was read from reports nothing. `declscope` says so only when a nested `only` was cancelled by one above it, so the quiet cases stay quiet. `declscope inspect` lists the files each namespace was built from (`namespaces[].files`); a package whose files are missing from it is one the filter removed. +**A clean analyzer says nothing about `shrink`.** The analyzer never reports `overexported`, and `shrink` never reports what the analyzer does. Run both, `shrink` first. + **A zero from `boundary` may be the switch, not the code.** `rules.boundary: off` silences the rule entirely, and the run looks like a clean repository. Read every config before reporting a count, the same way you would for `qualify`. **`-fix` widens; it does not draw boundaries.** On a codebase with boundary findings, `declscope -fix ./...` inserts `//declscope:package` above every crossed declaration — the wholesale widening step 2 of the order of work exists to avoid. Run `-fix -diff` first and read it. Its place in an adoption is renaming, after the structure is settled, and only where `names[].fixable` is true. @@ -332,9 +389,10 @@ cp -r repo /tmp/try-a # and measure there ## Order of work 1. `declscope survey ./...`, and read Checks in force before any count -2. Clear `boundary` by moving the boundary, not by widening everything -3. Re-measure with `survey`. Naming often falls with it, since merging two namespaces into one takes `ondemand` out of force -4. Fix the file names that do not match their contents -5. Rename what is left, in natural word order -6. Delete the baseline -7. Check the core count, and `go build`, `go test` and `declscope` in that order +2. If the repository has `internal/` packages, run `declscope shrink ./...` and settle it before anything else. Its fixes add boundary reports, and nothing that follows adds reports back +3. Clear `boundary` by moving the boundary, not by widening everything +4. Re-measure with `survey`. Naming often falls with it, since merging two namespaces into one takes `ondemand` out of force +5. Fix the file names that do not match their contents +6. Rename what is left, in natural word order +7. Delete the baseline +8. Check the core count, and `go build`, `go test`, `declscope shrink` and `declscope` in that order diff --git a/mise.toml b/mise.toml index 8dfb3af..9d52ccc 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -"github:mpyw/declscope" = "0.12.0" +"github:mpyw/declscope" = "0.13.0" From 5b886c138640aeb495cd6dcf4e7c14bb69867314 Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 13:25:27 +0900 Subject: [PATCH 2/6] chore: unexport what declscope shrink finds nothing outside the package uses declscope shrink -fix unexported 32 declarations in 8 internal/ packages. Nine of them are used from another file of the same package, and say so with //declscope:package and a "Shared on purpose" line naming the user: compileTransforms and compileMock (executor), newReadonlyHeaders, newWritableHeaders, newRequest, newResponse and errJSTimeout (js), ValueParser.parse (mock), and errBadRequest (server/body). commands.Version stays exported with //declscope:ignore overexported: .goreleaser.yaml sets it with -X by its exported path. Doc comments that named a renamed declaration by its old name are updated, including the core-file notes in config.go and body/parser.go that listed config.Parse and body.MaxBodySize as the package's API. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KtUXCXiWE99YCb9ZcLmSsn --- internal/cli/commands/app.go | 32 ++++++++++++------------ internal/config/config.go | 8 +++--- internal/config/config_test.go | 34 +++++++++++++------------- internal/config/error.go | 2 +- internal/config/types.go | 8 +++--- internal/config/types_test.go | 4 +-- internal/db/db.go | 6 ++--- internal/executor/compile.go | 18 ++++++++++---- internal/executor/executor.go | 26 ++++++++++---------- internal/executor/executor_test.go | 12 ++++----- internal/executor/mutation.go | 10 ++++---- internal/executor/query.go | 10 ++++---- internal/js/apply.go | 8 +++--- internal/js/apply_test.go | 6 ++--- internal/js/headers.go | 22 +++++++++++------ internal/js/headers_test.go | 38 ++++++++++++++--------------- internal/js/pool.go | 4 +-- internal/js/pool_test.go | 6 ++--- internal/js/request.go | 9 ++++--- internal/js/request_test.go | 4 +-- internal/js/response.go | 9 ++++--- internal/js/response_test.go | 14 +++++------ internal/js/timeout.go | 24 ++++++++++-------- internal/mock/csv.go | 6 ++--- internal/mock/filter.go | 2 +- internal/mock/mock.go | 2 +- internal/mock/value_parser.go | 9 ++++--- internal/server/body/form.go | 8 +++--- internal/server/body/json.go | 2 +- internal/server/body/parser.go | 32 +++++++++++++----------- internal/server/body/parser_test.go | 16 ++++++------ internal/server/handler.go | 8 +++--- internal/server/mutation_handler.go | 22 ++++++++--------- internal/server/query_handler.go | 22 ++++++++--------- internal/server/server.go | 6 ++--- 35 files changed, 241 insertions(+), 208 deletions(-) diff --git a/internal/cli/commands/app.go b/internal/cli/commands/app.go index 2ec4f5b..8793b90 100644 --- a/internal/cli/commands/app.go +++ b/internal/cli/commands/app.go @@ -25,30 +25,32 @@ import ( "github.com/mpyw/sql-http-proxy/internal/server" ) -// ShutdownTimeout is the maximum time to wait for graceful shutdown. -const ShutdownTimeout = 30 * time.Second +// shutdownTimeout is the maximum time to wait for graceful shutdown. +const shutdownTimeout = 30 * time.Second // Request header limits. These are stricter than the net/http defaults // (1MiB / 500 values) because every endpoint here is a SQL query keyed off a // small set of parameters - no legitimate client needs a large header block. const ( - // MaxHeaderBytes caps the total size of the request header block. - MaxHeaderBytes = 64 * 1024 - // MaxHeaderValueCount caps the number of header values in a request, + // maxHeaderBytes caps the total size of the request header block. + maxHeaderBytes = 64 * 1024 + // maxHeaderValueCount caps the number of header values in a request, // bounding the per-connection allocation a client can force. - MaxHeaderValueCount = 100 + maxHeaderValueCount = 100 ) -// ReadHeaderTimeout is the maximum time allowed to read request headers. +// readHeaderTimeout is the maximum time allowed to read request headers. // Only the header phase is bounded: bodies and responses are left untimed so // that large uploads and slow queries are not cut off mid-flight. -const ReadHeaderTimeout = 10 * time.Second +const readHeaderTimeout = 10 * time.Second // Version is set by goreleaser via ldflags. +// +//declscope:ignore overexported // .goreleaser.yaml sets it with -X, which names it by its exported path var Version = "dev" -// MakeApp creates a new CLI application instance. -func MakeApp() *cli.Command { +// makeApp creates a new CLI application instance. +func makeApp() *cli.Command { return &cli.Command{ Name: "sql-http-proxy", Usage: "YAML configuration-based HTTP to SQL proxy server", @@ -122,9 +124,9 @@ func action(ctx context.Context, cmd *cli.Command) error { srv := &http.Server{ Addr: listen, Handler: mux, - ReadHeaderTimeout: ReadHeaderTimeout, - MaxHeaderBytes: MaxHeaderBytes, - MaxHeaderValueCount: MaxHeaderValueCount, + ReadHeaderTimeout: readHeaderTimeout, + MaxHeaderBytes: maxHeaderBytes, + MaxHeaderValueCount: maxHeaderValueCount, } // Channel to receive server errors @@ -151,7 +153,7 @@ func action(ctx context.Context, cmd *cli.Command) error { } // Graceful shutdown with timeout - shutdownCtx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() slog.Info("Shutting down server...") @@ -164,4 +166,4 @@ func action(ctx context.Context, cmd *cli.Command) error { } // App is the main CLI application. -var App = MakeApp() +var App = makeApp() diff --git a/internal/config/config.go b/internal/config/config.go index 1d36ba8..0494eeb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,5 +1,5 @@ // config.go is the unit this package is named for: the Config document and -// its parsing. Its API is read as config.Parse / config.Config, so its names +// its parsing. Its API is read as config.Config, so its names // stay unprefixed. // //declscope:core @@ -236,8 +236,8 @@ func (cfg *Config) ValidateTransforms() error { return err } -// Parse parses configuration from YAML bytes. -func Parse(data []byte) (Config, error) { +// parse parses configuration from YAML bytes. +func parse(data []byte) (Config, error) { // Parse YAML to generic interface for schema validation var raw any if err := yaml.Unmarshal(data, &raw); err != nil { @@ -280,5 +280,5 @@ func ParseFile(filename string) (Config, error) { if err != nil { return Config{}, fmt.Errorf("failed to open file: %w", err) } - return Parse(data) + return parse(data) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7c47fa6..6cc6f63 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -243,7 +243,7 @@ queries: path: /user sql: SELECT * FROM users WHERE id = :id ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) assert.Equal(t, "postgres://localhost:5432/db", cfg.DSN()) assert.Len(t, cfg.Queries, 1) @@ -260,7 +260,7 @@ queries: id: 1 name: Alice ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) assert.Len(t, cfg.Queries, 1) assert.NotNil(t, cfg.Queries[0].Mock) @@ -268,7 +268,7 @@ queries: t.Run("invalid yaml", func(t *testing.T) { yaml := `invalid: yaml: syntax` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) }) @@ -278,7 +278,7 @@ queries: - path: /user sql: SELECT * FROM users ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "type") }) @@ -290,7 +290,7 @@ queries: path: /user sql: SELECT * FROM users ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) }) @@ -304,7 +304,7 @@ queries: object: id: 1 ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "sql") assert.Contains(t, err.Error(), "mock") @@ -318,7 +318,7 @@ queries: - type: one path: /user ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) }) @@ -333,7 +333,7 @@ queries: path: /user sql: SELECT 1 ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) assert.Equal(t, "postgres://myhost:5433/db", cfg.DSN()) }) @@ -347,7 +347,7 @@ queries: path: /user sql: SELECT 1 ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) assert.Equal(t, "postgres://localhost:5432/db", cfg.DSN()) }) @@ -365,7 +365,7 @@ queries: - id: 1 - id: 2 ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "filter") assert.Contains(t, err.Error(), "array") @@ -380,7 +380,7 @@ queries: object: id: 1 ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "object") assert.Contains(t, err.Error(), "many") @@ -393,7 +393,7 @@ mutations: path: /delete mock: true ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) require.Len(t, cfg.Mutations, 1) require.NotNil(t, cfg.Mutations[0].Mock) @@ -409,7 +409,7 @@ mutations: object: id: 1 ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) // type: none only allows mock: true, not mock: {object: ...} assert.Contains(t, err.Error(), "validation") @@ -425,7 +425,7 @@ queries: - id: 1 array_js: "return [{id: 1}]" ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "one source") }) @@ -441,7 +441,7 @@ queries: 1,Alice 2,Bob ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "filter") assert.Contains(t, err.Error(), "csv") @@ -487,7 +487,7 @@ queries: path: /user sql: SELECT * FROM users WHERE id = :id ` - _, err := Parse([]byte(yaml)) + _, err := parse([]byte(yaml)) require.Error(t, err) assert.Contains(t, err.Error(), "database.dsn") }) @@ -501,7 +501,7 @@ queries: object: id: 1 ` - cfg, err := Parse([]byte(yaml)) + cfg, err := parse([]byte(yaml)) require.NoError(t, err) assert.Empty(t, cfg.DSN()) }) diff --git a/internal/config/error.go b/internal/config/error.go index 93c99fb..e946070 100644 --- a/internal/config/error.go +++ b/internal/config/error.go @@ -18,7 +18,7 @@ var ( ) // formatValidationError converts a jsonschema ValidationError into a user-friendly message. -// It is the one entry point config.go (Parse) takes into this unit. +// It is the one entry point config.go (parse) takes into this unit. // //declscope:package func formatValidationError(err *jsonschema.ValidationError) string { diff --git a/internal/config/types.go b/internal/config/types.go index 97e07e0..f103866 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -69,8 +69,8 @@ const ( // Can be unmarshaled from either a string or array of strings. type AcceptTypes []AcceptType -// DefaultAcceptTypes is the default list when accepts is not specified. -var DefaultAcceptTypes = AcceptTypes{AcceptJSON, AcceptForm} +// defaultAcceptTypes is the default list when accepts is not specified. +var defaultAcceptTypes = AcceptTypes{AcceptJSON, AcceptForm} // UnmarshalYAML implements custom YAML unmarshaling for AcceptTypes. // Accepts either a string or an array of strings. @@ -117,7 +117,7 @@ func (q Query) GetMethod() string { // Returns default [json, form] if not specified, or empty slice if explicitly set to []. func (q Query) GetAccepts() AcceptTypes { if q.Accepts == nil { - return DefaultAcceptTypes + return defaultAcceptTypes } return *q.Accepts } @@ -151,7 +151,7 @@ func (m Mutation) GetMethod() string { // Returns default [json, form] if not specified, or empty slice if explicitly set to []. func (m Mutation) GetAccepts() AcceptTypes { if m.Accepts == nil { - return DefaultAcceptTypes + return defaultAcceptTypes } return *m.Accepts } diff --git a/internal/config/types_test.go b/internal/config/types_test.go index 53cfb75..ffdb643 100644 --- a/internal/config/types_test.go +++ b/internal/config/types_test.go @@ -261,7 +261,7 @@ func TestQuery_GetMethod(t *testing.T) { func TestQuery_GetAccepts(t *testing.T) { t.Run("default accepts when nil", func(t *testing.T) { q := Query{} - assert.Equal(t, DefaultAcceptTypes, q.GetAccepts()) + assert.Equal(t, defaultAcceptTypes, q.GetAccepts()) }) t.Run("custom accepts", func(t *testing.T) { @@ -315,7 +315,7 @@ func TestMutation_GetMethod(t *testing.T) { func TestMutation_GetAccepts(t *testing.T) { t.Run("default accepts when nil", func(t *testing.T) { m := Mutation{} - assert.Equal(t, DefaultAcceptTypes, m.GetAccepts()) + assert.Equal(t, defaultAcceptTypes, m.GetAccepts()) }) t.Run("custom accepts", func(t *testing.T) { diff --git a/internal/db/db.go b/internal/db/db.go index aebe702..8d7a026 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -18,8 +18,8 @@ import ( "github.com/mpyw/sql-http-proxy/internal/config" ) -// ConnectTimeout is the timeout for validating database connection. -const ConnectTimeout = 10 * time.Second +// connectTimeout is the timeout for validating database connection. +const connectTimeout = 10 * time.Second // Connect establishes a database connection based on the configuration. // Returns nil if the configuration doesn't require a database connection. @@ -42,7 +42,7 @@ func Connect(cfg config.Config, configDir string) (*sqlx.DB, error) { } // Validate connection with a ping - ctx, cancel := context.WithTimeout(context.Background(), ConnectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) defer cancel() if err := db.PingContext(ctx); err != nil { diff --git a/internal/executor/compile.go b/internal/executor/compile.go index 7ee3a4a..1ef4cf9 100644 --- a/internal/executor/compile.go +++ b/internal/executor/compile.go @@ -25,9 +25,13 @@ type CompileTransformOptions struct { ValueParser *mock.ValueParser } -// CompileTransforms compiles all transforms from a config.Transform. -// Note: mock is now compiled separately via CompileMock. -func CompileTransforms(t *config.Transform, opts CompileTransformOptions) (*CompiledTransforms, error) { +// compileTransforms compiles all transforms from a config.Transform. +// Note: mock is now compiled separately via compileMock. +// Shared on purpose: mutation.go (NewMutationExecutor) and query.go +// (NewQueryExecutor) compile their transforms here. +// +//declscope:package +func compileTransforms(t *config.Transform, opts CompileTransformOptions) (*CompiledTransforms, error) { if t == nil { return &CompiledTransforms{}, nil } @@ -59,8 +63,12 @@ func CompileTransforms(t *config.Transform, opts CompileTransformOptions) (*Comp return transforms, nil } -// CompileMock compiles a mock source from config.Mock. -func CompileMock(m *config.Mock, opts CompileTransformOptions) (mock.Source, error) { +// compileMock compiles a mock source from config.Mock. +// Shared on purpose: mutation.go (NewMutationExecutor) and query.go +// (NewQueryExecutor) compile their mock source here. +// +//declscope:package +func compileMock(m *config.Mock, opts CompileTransformOptions) (mock.Source, error) { if m == nil || m.IsEmpty() { return nil, nil } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index b8b2108..d0efab6 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -39,24 +39,24 @@ func (e *PhaseError) Unwrap() error { return e.Err } -// WrapPreError wraps an error as a pre-transform error. -func WrapPreError(err error) error { +// wrapPreError wraps an error as a pre-transform error. +func wrapPreError(err error) error { if err == nil { return nil } return &PhaseError{Phase: "pre", Err: err} } -// WrapMockError wraps an error as a mock-transform error. -func WrapMockError(err error) error { +// wrapMockError wraps an error as a mock-transform error. +func wrapMockError(err error) error { if err == nil { return nil } return &PhaseError{Phase: "mock", Err: err} } -// WrapPostError wraps an error as a post-transform error. -func WrapPostError(err error) error { +// wrapPostError wraps an error as a post-transform error. +func wrapPostError(err error) error { if err == nil { return nil } @@ -140,7 +140,7 @@ func (e *BaseExecutor[R]) ExecuteBase(reqCtx context.Context, params map[string] if e.Transforms.Pre != nil { result, err := e.Transforms.Pre.ApplyPre(ec.Ctx, ec.SQL, ec.Params, ec.TC) if err != nil { - return nil, WrapPreError(err) + return nil, wrapPreError(err) } ec.Params = result.Output ec.SQL = result.SQL @@ -168,7 +168,7 @@ func (e *BaseExecutor[R]) executeMock(ec *ExecContext[R]) (*R, error) { mockOutput, newCtx, err := e.MockSource.Data(ec.Ctx, ec.SQL, ec.Params, ec.TC) if err != nil { - return nil, WrapMockError(err) + return nil, wrapMockError(err) } if newCtx != nil { ec.Ctx = newCtx @@ -232,7 +232,7 @@ func (e *BaseExecutor[R]) ProcessNoneResult(ctx, originalParams map[string]any, if e.Transforms.PostAll != nil { _, err := e.Transforms.PostAll.ApplyPost(ctx, originalParams, nil, tc) if err != nil { - return nil, WrapPostError(err) + return nil, wrapPostError(err) } } // Return nil to trigger NoContent response @@ -248,7 +248,7 @@ func (e *BaseExecutor[R]) applyPostOne(ctx, params map[string]any, entry map[str } postResult, err := e.Transforms.PostAll.ApplyPost(ctx, params, entry, tc) if err != nil { - return nil, WrapPostError(err) + return nil, wrapPostError(err) } return postResult.Output, nil } @@ -261,7 +261,7 @@ func (e *BaseExecutor[R]) applyPostMany(ctx, params map[string]any, entries []ma if e.Transforms.PostEach != nil { eachResult, newCtx, err := e.Transforms.PostEach.ApplyPostToEachRow(currentCtx, params, currentEntries, tc) if err != nil { - return nil, WrapPostError(err) + return nil, wrapPostError(err) } result = eachResult currentCtx = newCtx @@ -270,7 +270,7 @@ func (e *BaseExecutor[R]) applyPostMany(ctx, params map[string]any, entries []ma for i, r := range eachResult { m, ok := r.(map[string]any) if !ok { - return nil, WrapPostError(errors.New("post.each transform must return object")) + return nil, wrapPostError(errors.New("post.each transform must return object")) } currentEntries[i] = m } @@ -279,7 +279,7 @@ func (e *BaseExecutor[R]) applyPostMany(ctx, params map[string]any, entries []ma if e.Transforms.PostAll != nil { postResult, err := e.Transforms.PostAll.ApplyPostToAllRows(currentCtx, params, currentEntries, tc) if err != nil { - return nil, WrapPostError(err) + return nil, wrapPostError(err) } result = postResult.Output } diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 7ec2285..6c88f40 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -9,13 +9,13 @@ import ( func TestWrapPreError(t *testing.T) { t.Run("nil error returns nil", func(t *testing.T) { - result := WrapPreError(nil) + result := wrapPreError(nil) require.Nil(t, result) }) t.Run("non-nil error wraps with phase", func(t *testing.T) { err := errors.New("test error") - result := WrapPreError(err) + result := wrapPreError(err) require.NotNil(t, result) var phaseErr *PhaseError @@ -28,13 +28,13 @@ func TestWrapPreError(t *testing.T) { func TestWrapMockError(t *testing.T) { t.Run("nil error returns nil", func(t *testing.T) { - result := WrapMockError(nil) + result := wrapMockError(nil) require.Nil(t, result) }) t.Run("non-nil error wraps with phase", func(t *testing.T) { err := errors.New("test error") - result := WrapMockError(err) + result := wrapMockError(err) require.NotNil(t, result) var phaseErr *PhaseError @@ -45,13 +45,13 @@ func TestWrapMockError(t *testing.T) { func TestWrapPostError(t *testing.T) { t.Run("nil error returns nil", func(t *testing.T) { - result := WrapPostError(nil) + result := wrapPostError(nil) require.Nil(t, result) }) t.Run("non-nil error wraps with phase", func(t *testing.T) { err := errors.New("test error") - result := WrapPostError(err) + result := wrapPostError(err) require.NotNil(t, result) var phaseErr *PhaseError diff --git a/internal/executor/mutation.go b/internal/executor/mutation.go index 7485ee3..4ffc726 100644 --- a/internal/executor/mutation.go +++ b/internal/executor/mutation.go @@ -55,12 +55,12 @@ type MutationExecutor struct { // NewMutationExecutor creates a new MutationExecutor with pre-compiled transforms. func NewMutationExecutor(db *sqlx.DB, mutation config.Mutation, opts CompileTransformOptions) (*MutationExecutor, error) { - transforms, err := CompileTransforms(mutation.Transform, opts) + transforms, err := compileTransforms(mutation.Transform, opts) if err != nil { return nil, err } - mockSource, err := CompileMock(mutation.Mock, opts) + mockSource, err := compileMock(mutation.Mock, opts) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func (e *MutationExecutor) ExecuteDB(reqCtx context.Context, ec *ExecContext[Mut return ec.Base.ProcessManyResult(ec.Ctx, ec.OriginalParams, results, ec.TC) default: - return nil, ErrUnsupportedMutationType + return nil, errUnsupportedMutationType } } @@ -166,5 +166,5 @@ func (e *MutationExecutor) execMySQLMany(reqCtx context.Context, ec *ExecContext return ec.Base.ProcessManyResult(ec.Ctx, ec.OriginalParams, nil, ec.TC) } -// ErrUnsupportedMutationType is returned when mutation type is not supported. -var ErrUnsupportedMutationType = errors.New("unsupported mutation type") +// errUnsupportedMutationType is returned when mutation type is not supported. +var errUnsupportedMutationType = errors.New("unsupported mutation type") diff --git a/internal/executor/query.go b/internal/executor/query.go index 0c373ae..f12fa60 100644 --- a/internal/executor/query.go +++ b/internal/executor/query.go @@ -44,12 +44,12 @@ type QueryExecutor struct { // NewQueryExecutor creates a new QueryExecutor with pre-compiled transforms. func NewQueryExecutor(db *sqlx.DB, query config.Query, opts CompileTransformOptions) (*QueryExecutor, error) { - transforms, err := CompileTransforms(query.Transform, opts) + transforms, err := compileTransforms(query.Transform, opts) if err != nil { return nil, err } - mockSource, err := CompileMock(query.Mock, opts) + mockSource, err := compileMock(query.Mock, opts) if err != nil { return nil, err } @@ -111,9 +111,9 @@ func (e *QueryExecutor) ExecuteDB(reqCtx context.Context, ec *ExecContext[Execut return ec.Base.ProcessManyResult(ec.Ctx, ec.OriginalParams, results, ec.TC) default: - return nil, ErrUnsupportedQueryType + return nil, errUnsupportedQueryType } } -// ErrUnsupportedQueryType is returned when query type is not supported. -var ErrUnsupportedQueryType = errors.New("unsupported query type") +// errUnsupportedQueryType is returned when query type is not supported. +var errUnsupportedQueryType = errors.New("unsupported query type") diff --git a/internal/js/apply.go b/internal/js/apply.go index 1ce69ba..5643f6e 100644 --- a/internal/js/apply.go +++ b/internal/js/apply.go @@ -30,11 +30,11 @@ type TransformContext struct { func NewTransformContext(r *http.Request) *TransformContext { var req *Request if r != nil { - req = NewRequest(r) + req = newRequest(r) } return &TransformContext{ Request: req, - Response: NewResponse(), + Response: newResponse(), } } @@ -81,7 +81,7 @@ func (t *Transformer) setupVM(ctx map[string]any, tc *TransformContext, opts vmS } // runCallable runs the compiled program and calls it with the given arguments. -// Execution is limited to JSTimeout to prevent infinite loops. +// Execution is limited to jsTimeout to prevent infinite loops. func (t *Transformer) runCallable(vm *goja.Runtime, fnName string, args ...goja.Value) (goja.Value, error) { fn, err := vm.RunProgram(t.program) if err != nil { @@ -97,7 +97,7 @@ func (t *Transformer) runCallable(vm *goja.Runtime, fnName string, args ...goja. return callable(goja.Undefined(), args...) }) if err != nil { - if errors.Is(err, ErrJSTimeout) { + if errors.Is(err, errJSTimeout) { return nil, err } return nil, parseJSError(err) diff --git a/internal/js/apply_test.go b/internal/js/apply_test.go index 8c1ceac..726894b 100644 --- a/internal/js/apply_test.go +++ b/internal/js/apply_test.go @@ -290,11 +290,11 @@ func TestApplyPostToAllRows(t *testing.T) { } func TestJSTimeout(t *testing.T) { - t.Parallel() // JSTimeout is 5s; overlap with the other slow timeout test. + t.Parallel() // jsTimeout is 5s; overlap with the other slow timeout test. t.Run("infinite loop times out", func(t *testing.T) { // This test verifies that infinite loops are interrupted - // Note: JSTimeout is 5 seconds by default + // Note: jsTimeout is 5 seconds by default transformer, err := CompilePre(`while(true) {} return input`) require.NoError(t, err) @@ -305,6 +305,6 @@ func TestJSTimeout(t *testing.T) { nil, ) require.Error(t, err) - assert.True(t, errors.Is(err, ErrJSTimeout), "expected timeout error, got: %v", err) + assert.True(t, errors.Is(err, errJSTimeout), "expected timeout error, got: %v", err) }) } diff --git a/internal/js/headers.go b/internal/js/headers.go index 37a8416..69721af 100644 --- a/internal/js/headers.go +++ b/internal/js/headers.go @@ -16,22 +16,28 @@ type Headers struct { readonly bool } -// NewHeaders creates a new Headers object from http.Header. -func NewHeaders(h http.Header, readonly bool) *Headers { +// newHeaders creates a new Headers object from http.Header. +func newHeaders(h http.Header, readonly bool) *Headers { if h == nil { h = make(http.Header) } return &Headers{headers: h, readonly: readonly} } -// NewReadonlyHeaders creates a read-only Headers object. -func NewReadonlyHeaders(h http.Header) *Headers { - return NewHeaders(h, true) +// newReadonlyHeaders creates a read-only Headers object. +// Shared on purpose: request.go (newRequest) wraps the request headers with it. +// +//declscope:package +func newReadonlyHeaders(h http.Header) *Headers { + return newHeaders(h, true) } -// NewWritableHeaders creates a writable Headers object. -func NewWritableHeaders(h http.Header) *Headers { - return NewHeaders(h, false) +// newWritableHeaders creates a writable Headers object. +// Shared on purpose: response.go (newResponse) wraps the response headers with it. +// +//declscope:package +func newWritableHeaders(h http.Header) *Headers { + return newHeaders(h, false) } // Get returns the value for a header name, or null if not found. diff --git a/internal/js/headers_test.go b/internal/js/headers_test.go index 13d5a49..f87c052 100644 --- a/internal/js/headers_test.go +++ b/internal/js/headers_test.go @@ -11,30 +11,30 @@ import ( func TestNewHeaders(t *testing.T) { t.Run("nil header", func(t *testing.T) { - h := NewHeaders(nil, false) + h := newHeaders(nil, false) assert.NotNil(t, h) assert.NotNil(t, h.headers) }) t.Run("with header", func(t *testing.T) { header := http.Header{"Content-Type": []string{"application/json"}} - h := NewHeaders(header, false) + h := newHeaders(header, false) assert.Equal(t, "application/json", h.Get("Content-Type")) }) t.Run("readonly", func(t *testing.T) { - h := NewReadonlyHeaders(nil) + h := newReadonlyHeaders(nil) assert.True(t, h.readonly) }) t.Run("writable", func(t *testing.T) { - h := NewWritableHeaders(nil) + h := newWritableHeaders(nil) assert.False(t, h.readonly) }) } func TestHeaders_Get(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Content-Type": []string{"application/json"}, }, false) @@ -55,7 +55,7 @@ func TestHeaders_Get(t *testing.T) { } func TestHeaders_Has(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Content-Type": []string{"application/json"}, }, false) @@ -74,19 +74,19 @@ func TestHeaders_Has(t *testing.T) { func TestHeaders_Set(t *testing.T) { t.Run("writable", func(t *testing.T) { - h := NewWritableHeaders(nil) + h := newWritableHeaders(nil) h.Set("Content-Type", "text/plain") assert.Equal(t, "text/plain", h.Get("Content-Type")) }) t.Run("replaces existing", func(t *testing.T) { - h := NewWritableHeaders(http.Header{"Content-Type": []string{"application/json"}}) + h := newWritableHeaders(http.Header{"Content-Type": []string{"application/json"}}) h.Set("Content-Type", "text/plain") assert.Equal(t, "text/plain", h.Get("Content-Type")) }) t.Run("readonly ignored", func(t *testing.T) { - h := NewReadonlyHeaders(nil) + h := newReadonlyHeaders(nil) h.Set("Content-Type", "text/plain") assert.Nil(t, h.Get("Content-Type")) }) @@ -94,7 +94,7 @@ func TestHeaders_Set(t *testing.T) { func TestHeaders_Append(t *testing.T) { t.Run("writable", func(t *testing.T) { - h := NewWritableHeaders(nil) + h := newWritableHeaders(nil) h.Append("Accept", "text/plain") h.Append("Accept", "application/json") values := h.headers["Accept"] @@ -104,7 +104,7 @@ func TestHeaders_Append(t *testing.T) { }) t.Run("readonly ignored", func(t *testing.T) { - h := NewReadonlyHeaders(nil) + h := newReadonlyHeaders(nil) h.Append("Accept", "text/plain") assert.Nil(t, h.Get("Accept")) }) @@ -112,20 +112,20 @@ func TestHeaders_Append(t *testing.T) { func TestHeaders_Delete(t *testing.T) { t.Run("writable", func(t *testing.T) { - h := NewWritableHeaders(http.Header{"Content-Type": []string{"application/json"}}) + h := newWritableHeaders(http.Header{"Content-Type": []string{"application/json"}}) h.Delete("Content-Type") assert.Nil(t, h.Get("Content-Type")) }) t.Run("readonly ignored", func(t *testing.T) { - h := NewReadonlyHeaders(http.Header{"Content-Type": []string{"application/json"}}) + h := newReadonlyHeaders(http.Header{"Content-Type": []string{"application/json"}}) h.Delete("Content-Type") assert.Equal(t, "application/json", h.Get("Content-Type")) }) } func TestHeaders_Entries(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"text/plain", "text/html"}, }, false) @@ -139,7 +139,7 @@ func TestHeaders_Entries(t *testing.T) { } func TestHeaders_Keys(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"text/plain"}, }, false) @@ -152,7 +152,7 @@ func TestHeaders_Keys(t *testing.T) { } func TestHeaders_Values(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Accept": []string{"text/plain", "application/json"}, }, false) @@ -163,7 +163,7 @@ func TestHeaders_Values(t *testing.T) { } func TestHeaders_ForEach(t *testing.T) { - h := NewHeaders(http.Header{ + h := newHeaders(http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"text/plain"}, }, false) @@ -189,14 +189,14 @@ func TestHeaders_ForEach(t *testing.T) { func TestHeaders_ToHTTPHeader(t *testing.T) { original := http.Header{"Content-Type": []string{"application/json"}} - h := NewHeaders(original, false) + h := newHeaders(original, false) result := h.ToHTTPHeader() assert.Equal(t, original, result) } func TestHeaders_ToJSObject(t *testing.T) { vm := goja.New() - h := NewWritableHeaders(http.Header{ + h := newWritableHeaders(http.Header{ "Content-Type": []string{"application/json"}, }) diff --git a/internal/js/pool.go b/internal/js/pool.go index 0f246ed..0f174c9 100644 --- a/internal/js/pool.go +++ b/internal/js/pool.go @@ -76,7 +76,7 @@ func (p *PooledVM) newCallable() (*pooledCallable, error) { // goja.Value.ToBoolean or goja.Value.Export pins the conversion to the right // scope. globals are set on the runtime before the call; pass nil for none. // -// Execution is bounded by JSTimeout; exceeding it returns ErrJSTimeout. +// Execution is bounded by jsTimeout; exceeding it returns errJSTimeout. func (p *PooledVM) Call[T any](globals map[string]any, convert func(goja.Value) T, args ...any) (T, error) { var zero T @@ -114,7 +114,7 @@ func (p *PooledVM) Call[T any](globals map[string]any, convert func(goja.Value) return pc.callable(goja.Undefined(), jsArgs...) }) if err != nil { - reusable = !errors.Is(err, ErrJSTimeout) + reusable = !errors.Is(err, errJSTimeout) return zero, err } diff --git a/internal/js/pool_test.go b/internal/js/pool_test.go index 72ea896..d0f30ef 100644 --- a/internal/js/pool_test.go +++ b/internal/js/pool_test.go @@ -71,7 +71,7 @@ func TestRunWithTimeout_ClearsLateInterrupt(t *testing.T) { // otherwise a single near-timeout call would poison a pooled runtime for // every request that followed. vm := goja.New() - vm.Interrupt(ErrJSTimeout) + vm.Interrupt(errJSTimeout) _, err := runWithTimeout(vm, func() (goja.Value, error) { return goja.Undefined(), nil @@ -84,13 +84,13 @@ func TestRunWithTimeout_ClearsLateInterrupt(t *testing.T) { } func TestPooledVM_Call_Timeout(t *testing.T) { - t.Parallel() // JSTimeout is 5s; overlap with the other slow timeout test. + t.Parallel() // jsTimeout is 5s; overlap with the other slow timeout test. p := newTestPool(t, "maybeSpin", `(function(spin) { if (spin) { while (true) {} } return 42 })`) _, err := p.Call(nil, goja.Value.ToInteger, true) require.Error(t, err) - assert.True(t, errors.Is(err, ErrJSTimeout), "expected timeout error, got: %v", err) + assert.True(t, errors.Is(err, errJSTimeout), "expected timeout error, got: %v", err) // The timed-out runtime is discarded rather than pooled, so later calls // must still succeed instead of inheriting the interrupt. diff --git a/internal/js/request.go b/internal/js/request.go index e666f1a..a4d9ee6 100644 --- a/internal/js/request.go +++ b/internal/js/request.go @@ -16,14 +16,17 @@ type Request struct { headers *Headers } -// NewRequest creates a new Request object from an HTTP request. +// newRequest creates a new Request object from an HTTP request. // The URL is cloned so the value JS observes is a snapshot, independent of any // later rewriting of r.URL by middleware or the router. -func NewRequest(r *http.Request) *Request { +// Shared on purpose: apply.go (NewTransformContext) builds the JS request here. +// +//declscope:package +func newRequest(r *http.Request) *Request { return &Request{ method: r.Method, url: r.URL.Clone(), - headers: NewReadonlyHeaders(r.Header), + headers: newReadonlyHeaders(r.Header), } } diff --git a/internal/js/request_test.go b/internal/js/request_test.go index aa2830a..250d799 100644 --- a/internal/js/request_test.go +++ b/internal/js/request_test.go @@ -15,7 +15,7 @@ func TestRequest(t *testing.T) { req.Header.Set("X-Custom", "value") req.Header.Set("Content-Type", "application/json") - r := NewRequest(req) + r := newRequest(req) t.Run("Method", func(t *testing.T) { assert.Equal(t, "POST", r.Method()) @@ -37,7 +37,7 @@ func TestRequest_ToJSObject(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/users?id=123", nil) req.Header.Set("Authorization", "Bearer token") - r := NewRequest(req) + r := newRequest(req) vm := goja.New() obj := r.ToJSObject(vm) diff --git a/internal/js/response.go b/internal/js/response.go index a3d57d5..9a6cc07 100644 --- a/internal/js/response.go +++ b/internal/js/response.go @@ -15,12 +15,15 @@ type Response struct { headers *Headers } -// NewResponse creates a new Response object with default values. -func NewResponse() *Response { +// newResponse creates a new Response object with default values. +// Shared on purpose: apply.go (NewTransformContext) builds the JS response here. +// +//declscope:package +func newResponse() *Response { return &Response{ status: http.StatusOK, statusText: "OK", - headers: NewWritableHeaders(nil), + headers: newWritableHeaders(nil), } } diff --git a/internal/js/response_test.go b/internal/js/response_test.go index 6abeeeb..433e0cf 100644 --- a/internal/js/response_test.go +++ b/internal/js/response_test.go @@ -10,7 +10,7 @@ import ( ) func TestNewResponse(t *testing.T) { - r := NewResponse() + r := newResponse() assert.Equal(t, http.StatusOK, r.Status()) assert.Equal(t, "OK", r.StatusText()) @@ -19,7 +19,7 @@ func TestNewResponse(t *testing.T) { } func TestResponse_Status(t *testing.T) { - r := NewResponse() + r := newResponse() t.Run("default status is 200", func(t *testing.T) { assert.Equal(t, 200, r.Status()) @@ -57,7 +57,7 @@ func TestResponse_Status(t *testing.T) { } func TestResponse_StatusText(t *testing.T) { - r := NewResponse() + r := newResponse() t.Run("default statusText is OK", func(t *testing.T) { assert.Equal(t, "OK", r.StatusText()) @@ -70,7 +70,7 @@ func TestResponse_StatusText(t *testing.T) { } func TestResponse_Ok(t *testing.T) { - r := NewResponse() + r := newResponse() t.Run("Ok returns true for 2xx", func(t *testing.T) { r.SetStatus(200) @@ -99,7 +99,7 @@ func TestResponse_Ok(t *testing.T) { } func TestResponse_Headers(t *testing.T) { - r := NewResponse() + r := newResponse() t.Run("Headers returns writable headers", func(t *testing.T) { h := r.Headers() @@ -111,7 +111,7 @@ func TestResponse_Headers(t *testing.T) { } func TestResponse_ToHTTPHeader(t *testing.T) { - r := NewResponse() + r := newResponse() r.Headers().Set("X-Test", "test-value") httpHeader := r.ToHTTPHeader() @@ -119,7 +119,7 @@ func TestResponse_ToHTTPHeader(t *testing.T) { } func TestResponse_ToJSObject(t *testing.T) { - r := NewResponse() + r := newResponse() vm := goja.New() obj := r.ToJSObject(vm) diff --git a/internal/js/timeout.go b/internal/js/timeout.go index 2981118..fbb8f2f 100644 --- a/internal/js/timeout.go +++ b/internal/js/timeout.go @@ -8,14 +8,18 @@ import ( "github.com/dop251/goja" ) -// JSTimeout is the maximum execution time for JavaScript transforms. -const JSTimeout = 5 * time.Second +// jsTimeout is the maximum execution time for JavaScript transforms. +const jsTimeout = 5 * time.Second -// ErrJSTimeout is returned when JS execution exceeds JSTimeout. -var ErrJSTimeout = errors.New("JavaScript execution timeout") +// errJSTimeout is returned when JS execution exceeds jsTimeout. +// Shared on purpose: apply.go (runCallable) and pool.go (PooledVM.Call) +// recognize a timed-out run by it. +// +//declscope:package +var errJSTimeout = errors.New("JavaScript execution timeout") -// runWithTimeout runs fn with a watchdog that interrupts vm after JSTimeout, -// translating the interrupt into ErrJSTimeout. +// runWithTimeout runs fn with a watchdog that interrupts vm after jsTimeout, +// translating the interrupt into errJSTimeout. // // The watchdog is disarmed under a mutex and the interrupt flag is cleared // before returning. Both matter for reuse: goja.Runtime.Interrupt sets a flag @@ -32,13 +36,13 @@ func runWithTimeout(vm *goja.Runtime, fn func() (goja.Value, error)) (goja.Value var mu sync.Mutex done := false - timer := time.AfterFunc(JSTimeout, func() { + timer := time.AfterFunc(jsTimeout, func() { mu.Lock() defer mu.Unlock() if done { return } - vm.Interrupt(ErrJSTimeout) + vm.Interrupt(errJSTimeout) }) // Deferred so the runtime is left usable even if fn panics. @@ -53,8 +57,8 @@ func runWithTimeout(vm *goja.Runtime, fn func() (goja.Value, error)) (goja.Value result, err := fn() if err != nil { if interrupted, ok := errors.AsType[*goja.InterruptedError](err); ok { - if timeoutErr, ok := interrupted.Value().(error); ok && errors.Is(timeoutErr, ErrJSTimeout) { - return nil, ErrJSTimeout + if timeoutErr, ok := interrupted.Value().(error); ok && errors.Is(timeoutErr, errJSTimeout) { + return nil, errJSTimeout } } return nil, err diff --git a/internal/mock/csv.go b/internal/mock/csv.go index fbb8c4a..81865fe 100644 --- a/internal/mock/csv.go +++ b/internal/mock/csv.go @@ -24,7 +24,7 @@ type csvSource struct { // //declscope:package type parseCSVOptions struct { - ValueParser *ValueParser + valueParser *ValueParser } // parseCSV parses inline CSV data into a csvSource. @@ -112,8 +112,8 @@ func parseCSVReaderWithOptions(r io.Reader, opts parseCSVOptions) (*csvSource, e if i < actualCols { var val any var err error - if opts.ValueParser != nil { - val, err = opts.ValueParser.Parse(record[i]) + if opts.valueParser != nil { + val, err = opts.valueParser.parse(record[i]) if err != nil { return nil, fmt.Errorf("line %d, column %q: %w", lineNum, header, err) } diff --git a/internal/mock/filter.go b/internal/mock/filter.go index 78c5d09..30a7b25 100644 --- a/internal/mock/filter.go +++ b/internal/mock/filter.go @@ -87,7 +87,7 @@ func (f *jsFilteredSource) filterArray(arr []any, input map[string]any, ctx map[ } // evaluateFilter evaluates the filter function for a single row. -// Execution is limited to js.JSTimeout to prevent infinite loops. +// Execution is limited to the js package's timeout to prevent infinite loops. func (f *jsFilteredSource) evaluateFilter(row any, input map[string]any, ctx map[string]any) (bool, error) { // ctx is a free variable, refreshed per call because the pooled runtime may // still hold the previous caller's value. diff --git a/internal/mock/mock.go b/internal/mock/mock.go index 1469fad..e7a137d 100644 --- a/internal/mock/mock.go +++ b/internal/mock/mock.go @@ -55,7 +55,7 @@ func Compile(m *config.Mock, opts CompileOptions) (Source, error) { } // CSV parsing options - csvOpts := parseCSVOptions{ValueParser: opts.ValueParser} + csvOpts := parseCSVOptions{valueParser: opts.ValueParser} var source Source var err error diff --git a/internal/mock/value_parser.go b/internal/mock/value_parser.go index e798f04..4b237c6 100644 --- a/internal/mock/value_parser.go +++ b/internal/mock/value_parser.go @@ -29,8 +29,11 @@ func CompileValueParser(jsCode string, helpers *js.CompiledHelpers) (*ValueParse return &ValueParser{vms: js.NewPooledVM("value_parser", program, helpers)}, nil } -// Parse parses a single cell value using the custom JS. -// Execution is limited to js.JSTimeout to prevent infinite loops. -func (p *ValueParser) Parse(value string) (any, error) { +// parse parses a single cell value using the custom JS. +// Execution is limited to the js package's timeout to prevent infinite loops. +// Shared on purpose: csv.go (parseCSVReaderWithOptions) parses each cell with it. +// +//declscope:package +func (p *ValueParser) parse(value string) (any, error) { return p.vms.Call(nil, goja.Value.Export, value) } diff --git a/internal/server/body/form.go b/internal/server/body/form.go index bafed69..d8b284c 100644 --- a/internal/server/body/form.go +++ b/internal/server/body/form.go @@ -24,7 +24,7 @@ func parseFormURLEncoded(body io.Reader, charsetName string) (map[string]any, er values, err := url.ParseQuery(string(data)) if err != nil { - return nil, fmt.Errorf("%w: invalid form data: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: invalid form data: %v", errBadRequest, err) } return formValuesToMap(values), nil @@ -44,7 +44,7 @@ func parseMultipartForm(body io.Reader, boundary, charsetName string) (map[strin break } if err != nil { - return nil, fmt.Errorf("%w: failed to read multipart: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: failed to read multipart: %v", errBadRequest, err) } // Skip file uploads (only process form fields) @@ -68,14 +68,14 @@ func parseMultipartForm(body io.Reader, boundary, charsetName string) (map[strin slog.Warn("Failed to close multipart part", "error", closeErr) } if err != nil { - return nil, fmt.Errorf("%w: failed to read multipart field: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: failed to read multipart field: %v", errBadRequest, err) } // Apply charset conversion if specified if charsetName != "" { data, err = charset.ToUTF8(data, charsetName) if err != nil { - return nil, fmt.Errorf("%w: charset conversion failed: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: charset conversion failed: %v", errBadRequest, err) } } diff --git a/internal/server/body/json.go b/internal/server/body/json.go index 17878a5..bba4bc0 100644 --- a/internal/server/body/json.go +++ b/internal/server/body/json.go @@ -41,7 +41,7 @@ func parseJSONBytes(data []byte) (map[string]any, error) { var result map[string]any if err := json.Unmarshal(data, &result); err != nil { - return nil, fmt.Errorf("%w: invalid JSON: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: invalid JSON: %v", errBadRequest, err) } return result, nil diff --git a/internal/server/body/parser.go b/internal/server/body/parser.go index bc94fc7..fcbbe61 100644 --- a/internal/server/body/parser.go +++ b/internal/server/body/parser.go @@ -1,6 +1,6 @@ // parser.go is the unit this package is named for: Parser reads the // Content-Type and dispatches to the format files beside it, and the API is -// read as body.NewParser / body.MaxBodySize without a prefix. +// read as body.NewParser without a prefix. // //declscope:core @@ -19,14 +19,18 @@ import ( "github.com/mpyw/sql-http-proxy/internal/config" ) -// MaxBodySize is the maximum allowed request body size (10MB). -const MaxBodySize = 10 * 1024 * 1024 +// maxBodySize is the maximum allowed request body size (10MB). +const maxBodySize = 10 * 1024 * 1024 // Error types var ( ErrUnsupportedMediaType = errors.New("unsupported media type") - ErrBadRequest = errors.New("bad request") - ErrBodyTooLarge = errors.New("request body too large") + // errBadRequest is shared on purpose: form.go and json.go wrap their + // parse errors with it. + // + //declscope:package + errBadRequest = errors.New("bad request") + ErrBodyTooLarge = errors.New("request body too large") ) // Parser parses HTTP request bodies based on Content-Type. @@ -41,11 +45,11 @@ func NewParser(accepts []config.AcceptType) *Parser { // Parse parses the request body based on Content-Type. // Returns ErrUnsupportedMediaType if Content-Type is not in accepts. -// Returns ErrBadRequest if body parsing fails. -// Returns ErrBodyTooLarge if body exceeds MaxBodySize. +// Returns errBadRequest if body parsing fails. +// Returns ErrBodyTooLarge if body exceeds maxBodySize. func (p *Parser) Parse(r *http.Request) (map[string]any, error) { // Limit body size to prevent DoS - body := io.LimitReader(r.Body, MaxBodySize+1) + body := io.LimitReader(r.Body, maxBodySize+1) contentType := r.Header.Get("Content-Type") if contentType == "" { @@ -67,7 +71,7 @@ func (p *Parser) Parse(r *http.Request) (map[string]any, error) { mediaType, params, err := mime.ParseMediaType(contentType) if err != nil { - return nil, fmt.Errorf("%w: invalid Content-Type: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: invalid Content-Type: %v", errBadRequest, err) } charsetName := params["charset"] @@ -91,7 +95,7 @@ func (p *Parser) Parse(r *http.Request) (map[string]any, error) { } boundary := params["boundary"] if boundary == "" { - return nil, fmt.Errorf("%w: missing boundary in multipart/form-data", ErrBadRequest) + return nil, fmt.Errorf("%w: missing boundary in multipart/form-data", errBadRequest) } return parseMultipartForm(body, boundary, charsetName) @@ -101,7 +105,7 @@ func (p *Parser) Parse(r *http.Request) (map[string]any, error) { } // readBody reads the body and applies charset conversion. -// Returns ErrBodyTooLarge if body exceeds MaxBodySize. +// Returns ErrBodyTooLarge if body exceeds maxBodySize. // Shared on purpose: every format parser (form.go, json.go) drains the body // through this one size- and charset-checked reader. // @@ -109,16 +113,16 @@ func (p *Parser) Parse(r *http.Request) (map[string]any, error) { func readBody(body io.Reader, charsetName string) ([]byte, error) { data, err := io.ReadAll(body) if err != nil { - return nil, fmt.Errorf("%w: failed to read body: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: failed to read body: %v", errBadRequest, err) } - if len(data) > MaxBodySize { + if len(data) > maxBodySize { return nil, ErrBodyTooLarge } if charsetName != "" { data, err = charset.ToUTF8(data, charsetName) if err != nil { - return nil, fmt.Errorf("%w: charset conversion failed: %v", ErrBadRequest, err) + return nil, fmt.Errorf("%w: charset conversion failed: %v", errBadRequest, err) } } diff --git a/internal/server/body/parser_test.go b/internal/server/body/parser_test.go index 32004b6..0e4c9e1 100644 --- a/internal/server/body/parser_test.go +++ b/internal/server/body/parser_test.go @@ -67,7 +67,7 @@ func TestParser_Parse_JSON(t *testing.T) { _, err := p.Parse(req) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) }) } @@ -96,7 +96,7 @@ func TestParser_Parse_JSON_RejectsIllFormed(t *testing.T) { _, err := p.Parse(req) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) assert.Contains(t, err.Error(), "invalid JSON") }) } @@ -109,7 +109,7 @@ func TestParser_Parse_JSON_RejectsTrailingContent(t *testing.T) { _, err := p.Parse(req) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) } func TestParser_Parse_FormURLEncoded(t *testing.T) { @@ -189,7 +189,7 @@ func TestParser_Parse_Multipart(t *testing.T) { _, err := p.Parse(req) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) }) t.Run("skips file uploads", func(t *testing.T) { @@ -289,7 +289,7 @@ func TestParser_Parse_InvalidContentType(t *testing.T) { _, err := p.Parse(req) require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) } func TestReadBody(t *testing.T) { @@ -300,7 +300,7 @@ func TestReadBody(t *testing.T) { }) t.Run("body too large", func(t *testing.T) { - largeData := make([]byte, MaxBodySize+1) + largeData := make([]byte, maxBodySize+1) _, err := readBody(bytes.NewReader(largeData), "") require.Error(t, err) assert.ErrorIs(t, err, ErrBodyTooLarge) @@ -309,7 +309,7 @@ func TestReadBody(t *testing.T) { t.Run("read error", func(t *testing.T) { _, err := readBody(&errorReader{}, "") require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) }) t.Run("charset conversion", func(t *testing.T) { @@ -322,7 +322,7 @@ func TestReadBody(t *testing.T) { t.Run("invalid charset", func(t *testing.T) { _, err := readBody(strings.NewReader("hello"), "invalid-charset-xyz") require.Error(t, err) - assert.ErrorIs(t, err, ErrBadRequest) + assert.ErrorIs(t, err, errBadRequest) }) } diff --git a/internal/server/handler.go b/internal/server/handler.go index 8bec9a6..d152858 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -43,11 +43,11 @@ type handledQueryRecorder func(record handledQueryRecord) // //declscope:package type handlerOptions struct { - ConfigDir string // Directory of config file for resolving relative paths - Helpers *js.CompiledHelpers // Global JavaScript helpers - ValueParser *mock.ValueParser // Global CSV value parser + configDir string // Directory of config file for resolving relative paths + helpers *js.CompiledHelpers // Global JavaScript helpers + valueParser *mock.ValueParser // Global CSV value parser //declscope:private - Recorder handledQueryRecorder + recorder handledQueryRecorder } // createNotFoundHandler creates a 404 handler. diff --git a/internal/server/mutation_handler.go b/internal/server/mutation_handler.go index 435b349..0e4ef8c 100644 --- a/internal/server/mutation_handler.go +++ b/internal/server/mutation_handler.go @@ -17,8 +17,8 @@ import ( "github.com/mpyw/sql-http-proxy/internal/server/body" ) -// MutationHandler handles HTTP requests for mutations. -type MutationHandler = baseHandler[executor.MutationResult] +// mutationHandler handles HTTP requests for mutations. +type mutationHandler = baseHandler[executor.MutationResult] // mutationHandlerProcessor implements HandlerResultProcessor for MutationResult. type mutationHandlerProcessor struct{} @@ -33,22 +33,22 @@ func (p *mutationHandlerProcessor) BuildResponse(result *executor.MutationResult return status, result.Data, result.ResponseHeader } -// NewMutationHandler creates a new MutationHandler. +// NewMutationHandler creates a new mutationHandler. // db can be nil if mock is configured. -func NewMutationHandler(db *sqlx.DB, mutation config.Mutation) (*MutationHandler, error) { +func NewMutationHandler(db *sqlx.DB, mutation config.Mutation) (*mutationHandler, error) { return newMutationHandlerWithOptions(db, mutation, handlerOptions{}) } -// newMutationHandlerWithOptions creates a new MutationHandler with options. +// newMutationHandlerWithOptions creates a new mutationHandler with options. // db can be nil if mock is configured. // Shared on purpose: server.go (NewServeMux) builds every mutation route here. // //declscope:package -func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts handlerOptions) (*MutationHandler, error) { +func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts handlerOptions) (*mutationHandler, error) { execOpts := executor.CompileTransformOptions{ - ConfigDir: opts.ConfigDir, - Helpers: opts.Helpers, - ValueParser: opts.ValueParser, + ConfigDir: opts.configDir, + Helpers: opts.helpers, + ValueParser: opts.valueParser, } exec, err := executor.NewMutationExecutor(db, mutation, execOpts) if err != nil { @@ -64,12 +64,12 @@ func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts h } } - return &MutationHandler{ + return &mutationHandler{ exec: exec, method: mutation.GetMethod(), pathParams: handlerPathParams(mutation.Path), parser: body.NewParser(mutation.GetAccepts()), - recorder: opts.Recorder, + recorder: opts.recorder, processor: &mutationHandlerProcessor{}, checkNotFound: false, delay: delay, diff --git a/internal/server/query_handler.go b/internal/server/query_handler.go index b42375d..5861b99 100644 --- a/internal/server/query_handler.go +++ b/internal/server/query_handler.go @@ -17,8 +17,8 @@ import ( "github.com/mpyw/sql-http-proxy/internal/server/body" ) -// QueryHandler handles HTTP requests for queries. -type QueryHandler = baseHandler[executor.ExecuteResult] +// queryHandler handles HTTP requests for queries. +type queryHandler = baseHandler[executor.ExecuteResult] // queryHandlerProcessor implements HandlerResultProcessor for ExecuteResult. type queryHandlerProcessor struct{} @@ -28,22 +28,22 @@ func (p *queryHandlerProcessor) BuildResponse(result *executor.ExecuteResult) (i return status, result.Output, result.ResponseHeader } -// NewQueryHandler creates a new QueryHandler. +// NewQueryHandler creates a new queryHandler. // db can be nil if mock is configured. -func NewQueryHandler(db *sqlx.DB, query config.Query) (*QueryHandler, error) { +func NewQueryHandler(db *sqlx.DB, query config.Query) (*queryHandler, error) { return newQueryHandlerWithOptions(db, query, handlerOptions{}) } -// newQueryHandlerWithOptions creates a new QueryHandler with options. +// newQueryHandlerWithOptions creates a new queryHandler with options. // db can be nil if mock is configured. // Shared on purpose: server.go (NewServeMux) builds every query route here. // //declscope:package -func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOptions) (*QueryHandler, error) { +func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOptions) (*queryHandler, error) { execOpts := executor.CompileTransformOptions{ - ConfigDir: opts.ConfigDir, - Helpers: opts.Helpers, - ValueParser: opts.ValueParser, + ConfigDir: opts.configDir, + Helpers: opts.helpers, + ValueParser: opts.valueParser, } exec, err := executor.NewQueryExecutor(db, query, execOpts) if err != nil { @@ -59,12 +59,12 @@ func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOpt } } - return &QueryHandler{ + return &queryHandler{ exec: exec, method: query.GetMethod(), pathParams: handlerPathParams(query.Path), parser: body.NewParser(query.GetAccepts()), - recorder: opts.Recorder, + recorder: opts.recorder, processor: &queryHandlerProcessor{}, checkNotFound: true, delay: delay, diff --git a/internal/server/server.go b/internal/server/server.go index acbe5ca..2579404 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -62,9 +62,9 @@ func NewServeMux(db *sqlx.DB, cfg config.Config, configDir string) (http.Handler } opts := handlerOptions{ - ConfigDir: configDir, - Helpers: helpers, - ValueParser: valueParser, + configDir: configDir, + helpers: helpers, + valueParser: valueParser, } for _, query := range cfg.Queries { From b22715d36e3dee3b1d2256c949bb30bed43802ff Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 13:25:28 +0900 Subject: [PATCH 3/6] ci: run declscope shrink before the analyzer Unexporting a declaration makes it private to its file's namespace, so a use from another file becomes a boundary crossing the analyzer reports. shrink therefore runs first in the declscope job. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KtUXCXiWE99YCb9ZcLmSsn --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea4023e..3569a38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,12 @@ jobs: - name: Set up mise uses: jdx/mise-action@v4 + # Before the analyzer: a declaration shrink unexports becomes private to + # its file's namespace, so a use from another file is then a boundary + # crossing the analyzer reports. + - name: Run declscope shrink + run: declscope shrink ./... + - name: Run declscope run: declscope ./... From e09d9192d0792b7a303fb556bb182b8261c29bd6 Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 15:16:12 +0900 Subject: [PATCH 4/6] chore: bump declscope to 0.13.1 --- .claude/skills/declscope-adoption/SKILL.md | 2 +- mise.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/declscope-adoption/SKILL.md b/.claude/skills/declscope-adoption/SKILL.md index 80800bd..f826fb0 100644 --- a/.claude/skills/declscope-adoption/SKILL.md +++ b/.claude/skills/declscope-adoption/SKILL.md @@ -10,7 +10,7 @@ x-embedded-digest: "sha256:f344c4e28af4379d944b3b6563f74727999fae4218804bb2bf0b5 # Adopting declscope -Written against **declscope 0.13.0**. Check the version first: this describes how that release behaves, not how an older one does. +Written against **declscope 0.13.1**. Check the version first: this describes how that release behaves, not how an older one does. ```bash declscope -V=full diff --git a/mise.toml b/mise.toml index 9d52ccc..87fe819 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -"github:mpyw/declscope" = "0.13.0" +"github:mpyw/declscope" = "0.13.1" From 2ca2d1abaefac3804870b8d53670809dde6a908d Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 15:17:10 +0900 Subject: [PATCH 5/6] chore: stamp the skill with 0.13.1 --- .claude/skills/declscope-adoption/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/declscope-adoption/SKILL.md b/.claude/skills/declscope-adoption/SKILL.md index f826fb0..7fb2802 100644 --- a/.claude/skills/declscope-adoption/SKILL.md +++ b/.claude/skills/declscope-adoption/SKILL.md @@ -3,7 +3,7 @@ name: declscope-adoption description: Adopt declscope on an existing Go codebase and drive its diagnostics to zero. Read this when introducing declscope to a repository, when clearing a declscope baseline, or when a declscope diagnostic is hard to act on. Covers reading the diagnostics as structure, the remedy for each shape, and the measurement traps that produce false confidence. license: MIT x-embedded-by: declscope -x-embedded-version: 0.13.0 +x-embedded-version: 0.13.1 x-embedded-at: "2026-09-25T04:22:46Z" x-embedded-digest: "sha256:f344c4e28af4379d944b3b6563f74727999fae4218804bb2bf0b57fa31d53093" --- From 2d4b931a5e61b658f2193db1ff55d55c6fc6077f Mon Sep 17 00:00:00 2001 From: "@mpyw" Date: Fri, 25 Sep 2026 15:18:01 +0900 Subject: [PATCH 6/6] chore: keep QueryHandler and MutationHandler exported declscope 0.13.1 carries an alias an exported constructor returns by its own name, so NewQueryHandler and NewMutationHandler keep returning a type their callers can name. --- internal/server/mutation_handler.go | 14 +++++++------- internal/server/query_handler.go | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/server/mutation_handler.go b/internal/server/mutation_handler.go index 0e4ef8c..f1dd897 100644 --- a/internal/server/mutation_handler.go +++ b/internal/server/mutation_handler.go @@ -17,8 +17,8 @@ import ( "github.com/mpyw/sql-http-proxy/internal/server/body" ) -// mutationHandler handles HTTP requests for mutations. -type mutationHandler = baseHandler[executor.MutationResult] +// MutationHandler handles HTTP requests for mutations. +type MutationHandler = baseHandler[executor.MutationResult] // mutationHandlerProcessor implements HandlerResultProcessor for MutationResult. type mutationHandlerProcessor struct{} @@ -33,18 +33,18 @@ func (p *mutationHandlerProcessor) BuildResponse(result *executor.MutationResult return status, result.Data, result.ResponseHeader } -// NewMutationHandler creates a new mutationHandler. +// NewMutationHandler creates a new MutationHandler. // db can be nil if mock is configured. -func NewMutationHandler(db *sqlx.DB, mutation config.Mutation) (*mutationHandler, error) { +func NewMutationHandler(db *sqlx.DB, mutation config.Mutation) (*MutationHandler, error) { return newMutationHandlerWithOptions(db, mutation, handlerOptions{}) } -// newMutationHandlerWithOptions creates a new mutationHandler with options. +// newMutationHandlerWithOptions creates a new MutationHandler with options. // db can be nil if mock is configured. // Shared on purpose: server.go (NewServeMux) builds every mutation route here. // //declscope:package -func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts handlerOptions) (*mutationHandler, error) { +func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts handlerOptions) (*MutationHandler, error) { execOpts := executor.CompileTransformOptions{ ConfigDir: opts.configDir, Helpers: opts.helpers, @@ -64,7 +64,7 @@ func newMutationHandlerWithOptions(db *sqlx.DB, mutation config.Mutation, opts h } } - return &mutationHandler{ + return &MutationHandler{ exec: exec, method: mutation.GetMethod(), pathParams: handlerPathParams(mutation.Path), diff --git a/internal/server/query_handler.go b/internal/server/query_handler.go index 5861b99..1df945f 100644 --- a/internal/server/query_handler.go +++ b/internal/server/query_handler.go @@ -17,8 +17,8 @@ import ( "github.com/mpyw/sql-http-proxy/internal/server/body" ) -// queryHandler handles HTTP requests for queries. -type queryHandler = baseHandler[executor.ExecuteResult] +// QueryHandler handles HTTP requests for queries. +type QueryHandler = baseHandler[executor.ExecuteResult] // queryHandlerProcessor implements HandlerResultProcessor for ExecuteResult. type queryHandlerProcessor struct{} @@ -28,18 +28,18 @@ func (p *queryHandlerProcessor) BuildResponse(result *executor.ExecuteResult) (i return status, result.Output, result.ResponseHeader } -// NewQueryHandler creates a new queryHandler. +// NewQueryHandler creates a new QueryHandler. // db can be nil if mock is configured. -func NewQueryHandler(db *sqlx.DB, query config.Query) (*queryHandler, error) { +func NewQueryHandler(db *sqlx.DB, query config.Query) (*QueryHandler, error) { return newQueryHandlerWithOptions(db, query, handlerOptions{}) } -// newQueryHandlerWithOptions creates a new queryHandler with options. +// newQueryHandlerWithOptions creates a new QueryHandler with options. // db can be nil if mock is configured. // Shared on purpose: server.go (NewServeMux) builds every query route here. // //declscope:package -func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOptions) (*queryHandler, error) { +func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOptions) (*QueryHandler, error) { execOpts := executor.CompileTransformOptions{ ConfigDir: opts.configDir, Helpers: opts.helpers, @@ -59,7 +59,7 @@ func newQueryHandlerWithOptions(db *sqlx.DB, query config.Query, opts handlerOpt } } - return &queryHandler{ + return &QueryHandler{ exec: exec, method: query.GetMethod(), pathParams: handlerPathParams(query.Path),