Skip to content

scaffold: go-service --with client (external-service client + httptest idiom)#27

Merged
markmals merged 5 commits into
mainfrom
scaffold-go-service-client
Jun 14, 2026
Merged

scaffold: go-service --with client (external-service client + httptest idiom)#27
markmals merged 5 commits into
mainfrom
scaffold-go-service-client

Conversation

@markmals

Copy link
Copy Markdown
Owner

Stacked on #26 (go-service sqlite). Base auto-retargets to main as the chain merges (#24#25#26 → this). Completes the trove-parity series.

Bundle 4 of 4. target add <name> --stack go-service --with client adds trove's internal/services idiom. Additive, pure stdlib (no deps/scripts) — composes with the base and every other --with.

What

A member-private internal/services package:

  • 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 an API 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. Tests are untagged (out of scenario scope under bindings: scoped) but run + pass.

Verification

Real target add: 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 · 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.

markmals added 5 commits June 14, 2026 15:06
…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.
@markmals markmals changed the base branch from scaffold-go-service-sqlite to main June 14, 2026 20:51
@markmals markmals merged commit ad473b9 into main Jun 14, 2026
4 checks passed
@markmals markmals deleted the scaffold-go-service-client branch June 14, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant