Skip to content

feat!: generate the client from the OpenAPI spec (project-scoped paths + inbox)#3

Merged
jeroenrinzema merged 7 commits into
mainfrom
feat/spec-codegen
Jun 25, 2026
Merged

feat!: generate the client from the OpenAPI spec (project-scoped paths + inbox)#3
jeroenrinzema merged 7 commits into
mainfrom
feat/spec-codegen

Conversation

@jeroenrinzema

@jeroenrinzema jeroenrinzema commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Migrates the Go SDK to spec-driven code generation against the platform's published OpenAPI spec, and in doing so consolidates the three open client-API PRs into one coherent breaking release.

This PR now supersedes and folds in:

Layered design

  • Generated low-level layer (gen/). Types are generated from the platform's OpenAPI spec with oapi-codegen v2.7.0 (the same tool the platform uses). gen/client.gen.go carries the DO NOT EDIT header; generation is wired via a //go:generate directive in gen/gen.go plus a make generate target.
  • Hand-written facade (root package). NewClient(apiKey, projectID string, opts ...Option) (*Client, error), the functional options, the central clientPath prefix injection (/api/client/projects/{projectID}), google/uuid project validation, and the Bearer auth header all stay single-point. The facade types are hand-tuned for ergonomics and documentation; they are not aliases of the generated types.
  • conformance_test.go is the contract that keeps the two layers in lockstep with the spec: every facade request/response type is asserted to round-trip to the exact JSON wire shape of its generated counterpart. Combined with the CI drift check, a spec change that alters a field, tag, or type surfaces as a failing test rather than a silent runtime mismatch.

Because oapi-codegen emits snake_case json tags matching the spec, there is no camelCase↔snake_case mapping layer — the generated types share the exact wire shape of the facade types.

Full client surface, including inbox

The facade now covers the complete Client API: users, organizations, events, scheduled resources, and inbox (user + organization). Inbox methods use the spec's surface — project-relative paths under clientPath:

Method Endpoint
PostUserInboxMessages / PostOrganizationInboxMessages POST .../{users,organizations}/inbox
GetUserInbox / GetOrganizationInbox GET .../inbox
GetUserInboxCount / GetOrganizationInboxCount GET .../inbox/count
MarkUserInboxRead / MarkOrganizationInboxRead POST .../inbox/read
MarkUserInboxArchived / MarkOrganizationInboxArchived POST .../inbox/archived

Note: the earlier inbox PR (#1) was stale against the spec — it used /inbox/opened and pre-project-in-URL paths. The spec uses /inbox/read + /inbox/archived under /projects/{projectID}, and the count endpoint takes only source/external_id/channel. The facade here follows the spec on every point, verified by conformance tests against the generated types.

Spec source / pin

spec/client.yaml is vendored from the platform and gen/ is generated from it. spec/SOURCE.md records the pinned source: the spec is pinned to the v0.1.0-rc.0 release's client.yaml asset — a stable, versioned, immutable source. Bumping to a future release is a one-line edit to spec/SOURCE.md; spec-sync.yml re-fetches at the pin weekly.

CI

  • ci.yml (push/PR): go generate ./... then git diff --exit-code gen (drift check), go build, go vet, gofmt -l ., go test -race.
  • spec-sync.yml (weekly cron + workflow_dispatch): re-fetches the spec at the pin, regenerates gen/, and opens a PR when the spec or generated output drifts.

Verification (green locally and in CI)

  • go generate ./... deterministic — git diff --exit-code gen clean.
  • go build ./..., go vet ./..., gofmt -l . clean; go test ./... pass (conformance + unit; integration tests skip without a live backend).
  • go mod tidy applied.

The ! marks the breaking surface: NewClient now requires a project ID and returns an error, every Client API path gains the /projects/{projectID} segment, and the build gains a generated low-level layer.

…tID}

The Lunogram Client API now scopes every endpoint to a project via the
URL: /api/client/... becomes /api/client/projects/{projectID}/....

NewClient now takes a required projectID (a UUID) and returns an error if
it is empty or malformed. The project segment is injected centrally in the
transport (clientPath), so resource methods pass project-relative paths.

This is a hard, intentional breaking change with no backwards compatibility.
It depends on the corresponding platform change that requires the project
in the URL, and should merge once that ships.
Migrate the SDK to spec-driven code generation, mirroring the js-sdk
template. The low-level type layer is now generated from the platform's
vendored OpenAPI spec; the hand-written facade (NewClient, functional
options, the /api/client/projects/{projectID} prefix, google/uuid
validation, Bearer auth) stays single-point and unchanged.

Layout:
- spec/client.yaml: vendored, pinned spec (see spec/SOURCE.md for the
  source repo, spec path, and pinned ref). No platform release needed —
  any ref is fetchable via the raw URL.
- gen/: spec-generated types (client.gen.go, DO-NOT-EDIT) via
  oapi-codegen v2.7.0, wired through go:generate and a go.mod tool
  dependency so `go generate` is deterministic with no separate install.
  gen/types.go is a hand-written companion for the spec's x-go-type
  references (pagination params), mirroring the platform's own pattern.
- conformance_test.go: asserts every facade request/response type shares
  the exact JSON wire shape of its generated spec counterpart, so a spec
  change surfaces as a failing test rather than a silent runtime mismatch.

CI:
- ci.yml: regenerate from the spec and fail on drift (git diff gen), then
  build, vet, gofmt, and test.
- spec-sync.yml: weekly cron + manual; re-fetch the spec at the pin in
  spec/SOURCE.md, regenerate, and open a PR on change.

The public API is unchanged from #2; the ! marks the build-system and
dependency shift (go.mod tool directive, generated low-level layer).
Expose the complete inbox surface on the hand-written facade, mirroring
the spec's user and organization inbox endpoints:

- PostUserInboxMessages / PostOrganizationInboxMessages
- GetUserInbox / GetOrganizationInbox (with InboxQuery filters)
- GetUserInboxCount / GetOrganizationInboxCount (with InboxCountQuery)
- MarkUserInboxRead / MarkOrganizationInboxRead
- MarkUserInboxArchived / MarkOrganizationInboxArchived

Add ergonomic facade types (InboxMessageCreate, InboxMessageRef, the
query structs, and the InboxMessage/InboxMessageList/InboxCount responses)
plus a url.Values query encoder, all kept in lockstep with the generated
types via conformance tests. Document the inbox methods in the README.
@jeroenrinzema jeroenrinzema changed the title refactor!: generate the client types from the OpenAPI spec feat!: generate the client from the OpenAPI spec (project-scoped paths + inbox) Jun 24, 2026
@jeroenrinzema jeroenrinzema changed the base branch from feat/project-in-url to main June 24, 2026 12:34
Flip the vendored client spec's provenance from the platform PR #262 branch
commit to the published v0.1.0-rc.0 release asset (client.yaml). The spec
content is byte-identical, so gen/ is unchanged; this only changes where the
spec is sourced from to a stable, versioned, immutable release.

spec-sync.yml now resolves the release asset download URL from SOURCE.md
instead of the raw-githubusercontent ref URL.
v0.1.0-rc.0 was a throwaway dry-run release and has been deleted from the
platform repo, so spec-sync's fetch URL 404s. Repin to the published
v0.1.0-rc.1 release. The vendored client.yaml is byte-identical to the
rc.1 asset, so gen/ is unchanged.
Bring the Go SDK to parity with the JS/Python SDKs on the full Client API
surface, and expand test coverage:

- RegisterDevice (POST /users/devices)
- GetVapidPublicKey (GET /push/vapid)
- CreateSession (POST /auth-methods/{id}/sessions)

Tests:
- conformance: DeviceRegistration, VapidPublicKey, CreateSessionRequest and
  SessionToken now round-trip against their generated spec types.
- integration: RegisterDevice, GetVapidPublicKey, CreateSession (the last
  gated on LUNOGRAM_AUTH_METHOD_ID), plus the previously untested
  MarkUserInboxRead/Archived and MarkOrganizationInboxRead/Archived.

README documents the new device/push/session methods.
@jeroenrinzema jeroenrinzema merged commit 00694e0 into main Jun 25, 2026
4 checks passed
@jeroenrinzema jeroenrinzema deleted the feat/spec-codegen branch June 25, 2026 15:49
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