scaffold: go-service --with client (external-service client + httptest idiom)#27
Merged
Conversation
…ion) go-service is now a `sharedModule` stack: members compose into ONE repo-root go.mod — each a cmd/<name> sharing internal/ packages, trove's shape — instead of a self-contained module per member. - `target add` creates the root go.mod if the repo isn't a Go module yet, with the module path derived from the git origin remote (host/owner/repo, e.g. github.com/markmals/trove) and falling back to the project dir base name. A second member just joins the existing module; members render no go.mod of their own. (scaffold.SharedModule + cmd/specify ensureRootGoMod/deriveModulePath.) - The phase-0 `go mod tidy` (run in the member dir) tidies the shared root module. Verified for real: two go-service members compose into one module, `go build ./...` from the root sees both cmd/, and each `specify verify` is green; the git remote drives the module path (git@github.com:acme/widgets → github.com/acme/widgets). First of the trove-parity bundles (OpenAPI/oapi-codegen, SQLite+config, and the service-client+httptest idiom follow).
…o.mod Consolidate the live tracker on this branch: racket-ui foundation (PR #22) and Slice 5 (PR #23) marked shipped (text matches those PRs byte-for-byte, so it merges cleanly in any order), plus the go-service shared root go.mod (PR #24) done and the remaining trove-parity bundles tracked (OpenAPI/oapi-codegen in progress, then SQLite+config, then service-client+httptest).
The trove-parity OpenAPI bundle. `target add <name> --stack go-service --with
openapi` produces a contract-first service in troved's shape:
- Ships openapi.yaml (the contract, source of truth, with an x-spec trace to the
story) + oapi-codegen.yaml (strict-server + std-http-server).
- Overwrites main.go/greeting.go/greeting_test.go with the generated-interface
wiring: a `server` implementing api.StrictServerInterface, returning typed
response objects; routed via api.HandlerFromMux(api.NewStrictHandler(...)).
- Phase-ordered install: `go get` the runtime + the oapi-codegen `tool` directive
(phase 1) → generate internal/api/api.gen.go (phase 2) → `go mod tidy` (the base
step moved to a later phase so codegen lands first). A conditional
`mise run generate` task regenerates from the contract.
- Members import the generated package by full module path, so Data gains Module
(resolveModulePath: the existing root go.mod's module line, else derived); it's
passed to the template + to ensureRootGoMod so the written go.mod and the
imports agree.
Also fixed: the default base main.go shipped a literal unrendered `{{kebab .Name}}`
in its package comment (it's a real compiled .go, not a template — made the doc
comment name-independent instead).
Verified green-on-arrival for real: `target add troved --with openapi` (git remote
→ module github.com/acme/troved) generates api.gen.go, `go build ./...` +
fmt:check + vet + `specify verify` (2 passed · 1 locked) all pass; the default
(no --with) and a second member still compose + verify green. mise run ci green
(the go-service files/ package compiles as part of the suite).
…fig) The trove-parity persistence bundle. Additive — like the web email/stripe features it overwrites no shared file, so it composes with the base AND --with openapi. Ships a member-private internal/store package: - store.go: pure-Go glebarez SQLite (no cgo → static binary), busy_timeout pragma + single open conn, env-resolved DSN, a settings KV (Setting/SetSetting). - migrate.go: //go:embed migrations/*.sql, applied once via a _migrations table, idempotent. - migrations/0001_init.sql: the settings table. - store_test.go: a migrate + KV roundtrip test (untagged → out of scenario scope under bindings:scoped, but it runs and must pass). Plus config.go: a flag/env config helper (loadConfig with envStr/envInt fallbacks for addr/port/db), ready to wire into main. Feature files are .tmpl so speckit's `go build ./...` doesn't try to compile them (they import glebarez, and config.go is a func-main-less main package); the dep is added by a `go get` script and tidied by the base phase-5 go mod tidy. Verified green-on-arrival for real: base+sqlite AND openapi+sqlite (17 files, both deps resolve) → go build / fmt:check / vet / specify verify (2 passed · 1 locked) all green. Recipe proven in a throwaway prototype (driver DSN, embed, idempotent migrate, wired + additive cases) before templatizing.
…t idiom)
The final trove-parity bundle. Additive, pure stdlib (no deps/scripts) — like
the sqlite/email/stripe features it overwrites no shared file, so it composes
with the base and every other --with.
Ships a member-private internal/services package (trove's internal/services
idiom):
- services.go: a bounded http.Client (DefaultTimeout), GetJSON (decode 2xx,
typed *APIError on non-2xx), and SanitizeURLError — which strips the *url.Error
wrapper (whose message embeds the full request URL) so a key passed in a query
string can't leak into logs.
- example.go: an example Client (New + GetThing) projecting only consumed fields,
routing every call through GetJSON.
- services_test.go: a fakeServer(t, routes) httptest harness (unmocked paths →
501 so unexpected calls fail loudly) + success and non-2xx-is-APIError tests.
The files are plain .go (no template vars), so speckit's own go build/go test
compile + run the package — free validation alongside the base files/ package.
The tests are untagged (out of scenario scope under bindings:scoped) but run +
pass.
Verified green-on-arrival for real: base+client AND the full openapi+sqlite+client
stack (internal/{api,services,store}, both deps) → go build / fmt:check / vet /
test / specify verify (2 passed · 1 locked) all green. This completes the
trove-parity series: root go.mod, openapi, sqlite, and client compose into one
troved-shaped service.
This was referenced Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #26 (go-service sqlite). Base auto-retargets to
mainas the chain merges (#24 → #25 → #26 → this). Completes the trove-parity series.Bundle 4 of 4.
target add <name> --stack go-service --with clientadds trove'sinternal/servicesidiom. Additive, pure stdlib (no deps/scripts) — composes with the base and every other--with.What
A member-private
internal/servicespackage:services.go— a boundedhttp.Client(DefaultTimeout),GetJSON(decode 2xx, typed*APIErroron non-2xx), andSanitizeURLErrorwhich strips the*url.Errorwrapper (whose message embeds the full request URL) so an API key passed in a query string can't leak into logs.example.go— an exampleClient(New+GetThing) projecting only consumed fields, routing every call throughGetJSON.services_test.go— afakeServer(t, routes)httptest harness (unmocked paths → 501 so unexpected calls fail loudly) + success and non-2xx-is-APIErrortests.The files are plain
.go(no template vars), so speckit's owngo build/go testcompile + run the package — free validation alongside the basefiles/package. Tests are untagged (out of scenario scope underbindings: scoped) but run + pass.Verification
Real
target add: base+client AND the fullopenapi+sqlite+clientstack (internal/{api,services,store}, both deps) →go build/fmt:check/vet/test/specify verify(2 passed · 0 failed · 1 locked) all green. Prototyped first.Series complete
go-service now covers trove's shape end to end: root
go.mod(#24) · OpenAPI/oapi-codegen (#25) · SQLite store+migrations+config (#26) · external-service client (#27) — composing into one troved-shaped, green-on-arrival service.