Skip to content

Establish the codegen target surface, with a petstore example as its witness #64

Description

@Wahbeh-Mohammad

Summary

@dexpace/core ships the assembly half of a code-generation target but not the
execution half. buildRequest turns an OperationDescriptor into a Request, and
Runtime, Paginator, typedSseStream, decodeSuccessResponse, and the auth steps each
do their job — but there is no seam that binds them, so a generated client has nothing to
delegate to and would have to hand-roll the wiring in every service SDK.

The Python SDK closed this with dexpace.sdk.core.codegen (see
docs/codegen-target.md),
whose contract is: the generator emits data and delegation, never logic. An operation
table plus a facade whose every method binds arguments into an input object and calls a
shared executor. All behaviour — request assembly, pipeline, retry, auth resolution, error
mapping, pagination, SSE — stays in core.

This issue proposes the Node equivalent, and proposes proving it with a compiling witness
before committing to the API.

What's missing

  1. No executor tier. Nothing exports an object with execute / executeRequest /
    paginate / events plus ownership-aware close (an owned transport is closed, a
    borrowed runtime is not). Runtime already implements Transport, so this layer is
    thin — but it does not exist.

  2. No declarative status-to-error map. toHttpError produces exactly one class,
    HttpStatusError. Every service SDK would grow its own hand-written
    if (status === ...) throw ... chain — precisely what the Python StatusErrorMap
    exists to prevent. Python additionally enforces at construction that a mapped class
    extends the response-error base and is disjoint from the transport-error branch, so a
    service error can never masquerade as a transport failure. Node needs the equivalent
    guarantee stated and enforced.

  3. OperationDescriptor merges the static and per-call halves. It carries
    pathParams, query, headers, and body inline, so it cannot be a module-level
    constant reused across calls the way a generated operation table needs. Python splits
    this into a frozen Operation (static) and an OperationInput (per-call). Resolving
    this is a compatibility question on an already-@public interface, so it wants a
    decision, not a patch.

  4. The operation auth tier has no source. docs/deferred-items.md records it as
    unscoped because "nothing in this roadmap ships a per-operation layer (no
    codegen/client surface)". perCall (via RequestOptions.auth) and client are both
    satisfiable today. A generated client is that missing surface, and it needs the
    strict resolution rule Python states: presence selects the tier, not satisfiability
    — a present-but-unsatisfiable tier fails loudly and never falls through to a broader
    one, so a caller who asked for a specific credential never gets a silent downgrade to
    the client default.

  5. FakeTransport and countingResponse are unreachable. They live in
    packages/core/src/testing/ and are not re-exported from the package entry point.
    Anyone testing against the public surface either deep-imports src/ — loading core
    twice and breaking every cross-boundary instanceof — or rewrites a fake transport.
    Worth deciding whether the testing helpers are public API.

Proposed approach

Build a throwaway examples/petstore/ spike first, outside packages/ and tests/ so no
CI script, bunfig glob, partition gate, or api-extractor report is touched. It mirrors the
Python witness: the same frozen OpenAPI document, a deterministic generator, a
projection-only facade, a regen-diff check, and an end-to-end canary over an in-memory
transport covering unary calls, a merge-patch body with three-valued fields, cursor
pagination, SSE with a [DONE] sentinel, all three auth-tier outcomes, and both close
semantics.

The spike's output is a findings list: for each gap above, what had to be hand-written and
whether it belongs in packages/core/src/codegen/. Only then does the core API get
designed — with a working consumer already in hand rather than guessed at.

Non-goals

  • Shipping a code generator. The generator here is a fixture that renders one frozen
    document; a real one is a separate project.
  • Generating models from components/schemas. Models stay hand-written, exactly as in the
    Python example — the generator reads operationId, method, path template, and the
    x-dexpace vendor extension, nothing else.
  • Making the example a published package or a CI-visible test.

References

  • Python contract: python-sdk/docs/codegen-target.md
  • Python witness: python-sdk/examples/petstore/
  • packages/core/src/seams/operation.ts — the half that exists
  • docs/deferred-items.md — the operation auth tier, recorded as unscoped

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions