Skip to content

chore(codegen): add openapi-to-dart http client generator#1503

Draft
spydon wants to merge 11 commits into
mainfrom
lukasklingsbo/sdk-1106-spike-smithytypespec-codegen-for-dartflutter-sdk-http-layer
Draft

chore(codegen): add openapi-to-dart http client generator#1503
spydon wants to merge 11 commits into
mainfrom
lukasklingsbo/sdk-1106-spike-smithytypespec-codegen-for-dartflutter-sdk-http-layer

Conversation

@spydon

@spydon spydon commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

DO NOT MERGE. Spike / proof of concept. Resolves the Dart/Flutter track of SDK-1106.

Summary

Explores whether the mechanical HTTP layer of supabase-dart (Storage, Functions, PostgREST) can be generated from the shared Smithy models in supabase/sdk#51, producing idiomatic Dart a maintainer would own long-term. Companion to the Swift spike (supabase/supabase-swift#1047).

Everything lives in a self-contained packages/supabase_openapi_generator package so nothing touches the shipped SDK yet. The package README documents how to use it; this description holds the spike analysis.

The generator is verified against every OpenAPI artifact currently in the SDK branch: Storage (18 ops), Functions (5), and the newly added PostgREST DatabaseService (7). All generated code formats and analyzes cleanly (dart format, dart analyze, and DCM) and is covered by tests.

Recommendation: adopt a small custom Dart emitter

It is the only option that meets all three hard constraints at once:

  1. streaming request bodies (Stream<List<int>>, mandatory for TUS and large mobile uploads),
  2. streaming responses (mandatory for Functions SSE/streaming),
  3. no build_runner and no dart:io (so Web/WASM works).

The emitter is bin/generate.dart. It reads the committed OpenAPI artifacts and writes http-based clients into lib/src/generated/. It builds the Dart source with package:code_builder and formats it in-process with package:dart_style (both maintained by the Dart team), so the emitter has no hand-rolled brace/comma bookkeeping and no shelling out to dart format. A thin hand-written runtime (ApiClient) owns transport and headers; generated code is pure request-building + response-decoding, mirroring the Swift split.

Alternatives evaluated (why not an existing tool)

1. Input model -> Dart client generators

None of the off-the-shelf generators meet streaming-both-ways + no-build_runner + web/WASM together.

Tool build_runner streaming request streaming response web/WASM Verdict
Smithy -> Dart (AWS Amplify smithy_codegen) Yes (on generated output) Yes Yes Yes (io/js split) Rejectsmithy runtime is published but "experimental, internal use only"; smithy_codegen is unpublished (publish_to: none); heavy toolchain (Java Smithy Docker -> AST -> Dart CLI -> build_runner); every client hard-depends on AWS packages (aws_common, aws_signature_v4). Good as a reference only.
Kiota (Microsoft, Dart target) No No (buffers via stream.toList()) No (buffers; no typed binary-response path at all) Yes Reject — Dart runtime buffers request and response bodies; Storage streaming is a hard blocker. Still pre-1.0 preview.
OpenAPI Generator dart No partial (upload only) No (buffered) No (api_client.mustache imports dart:io) Reject
OpenAPI Generator dart-dio Yes No No heavy (built_value + dio) Reject
swagger_dart_code_generator Yes No No chopper-based Reject
retrofit + dio Yes No Yes dio ok Reject
tonik (pure-Dart CLI, newest) No No No dio-based Closest to watch

The two capabilities almost nobody exposes are the two we need most: a Stream<List<int>> request body (only OpenAPI Generator's dart does it, upload-only, and that generator breaks web via dart:io) and a streaming response (only retrofit, which needs build_runner and can't stream requests). package:http gives both for free via StreamedRequest and Client.send(), is web/WASM-safe, and needs no build_runner — which is why we generate against it.

2. Dart code-emission library (how the generator writes Dart)

For emitting the Dart source itself, the standard is package:code_builder + package:dart_style (used by source_gen, json_serializable, freezed). This spike uses them: code_builder for class/field/constructor/method structure and automatic import handling, dart_style's DartFormatter for in-process formatting. Procedural method bodies are still supplied as code blocks (code_builder models structure, not arbitrary statement ASTs), so it is a hybrid, but the scaffolding is no longer hand-concatenated strings.

What's included

packages/supabase_openapi_generator/
  openapi/                     # artifacts copied verbatim from supabase/sdk#51
  bin/generate.dart            # the emitter (code_builder + dart_style)
  lib/src/runtime.dart         # hand-written transport: ApiClient, streaming, errors
  lib/src/generated/*.g.dart   # GENERATED Storage (18) + Functions (5) + Database (7)
  test/generated_client_test.dart  # proves the spike questions + correctness fixes

Reproduce:

cd packages/supabase_openapi_generator && dart pub get && dart run bin/generate.dart && dart test

Spike questions

# Question Answer
1 Streaming uploads, no buffering? Yeshttp.StreamedRequest, proven (TUS uploadChunk). Off-the-shelf can't.
2 Streaming responses? Yes — octet-stream returns StreamedApiResponse wrapping response.stream, proven.
3 Multipart with a streaming file part? Yeshttp.MultipartFile(field, stream, length), proven.
4 Middleware for runtime auth headers? Yes — per-request HeaderProvider, proven with a token that changes between calls.
5 Auth flows? Partly — not modelled yet; the HTTP ops would generate cleanly, the session loop stays hand-written.
6 PostgREST query builder? Transport only — see below; confirmed against the new DatabaseService model.
7 Web compatibility? Yes — only http, no dart:io, no conditional imports.
8 build_runner required? No — plain dart run; output committed. Decisive advantage over dart-dio.
9 Effort to build an emitter? Low — one small file (code_builder + dart_style) covered all three services.
10 Model gaps? See below.

PostgREST (question 6)

The branch now includes a DatabaseService model, so this was tested directly. The emitter produces a compiling DatabaseApi, but it confirms the earlier assessment: only the transport benefits, the query builder stays hand-written. Every PostgREST operation models its query semantics as stringly-typed query params (select, order, filters, on_conflict, …) plus a raw octet-stream body. The generated selectRows(...) is a thin transport method; the logic that builds .select()/.eq()/.order() into those params is exactly what codegen cannot express and must remain hand-written.

Model gaps found (for supabase/sdk#51)

  • Functions output should be @streaming (currently plain Blob -> format: byte). The emitter already streams application/octet-stream responses, but the trait makes intent explicit.
  • PostgREST bodies are modelled as octet-stream blobs, so generated methods take a raw Stream<List<int>> body rather than a typed row model. Acceptable for a transport layer, but worth noting.
  • Single-member outputs (ListBucketsResponseContent { items }) generate wrapper objects; the facade unwraps them.
  • Long/Integer both collapse to OpenAPI number -> Dart num. Works for JSON; int would read better. Not a blocker.

Code review

A high-effort review ran on the spike. The following correctness fixes were applied and each has a test:

  • Percent-encode path parameters (wildcards keep /), mirroring the storage fix in fix(storage): percent-encode object paths in request URLs #1479.
  • Tolerant numeric response-header parsing instead of force-unwrapping (TUS Upload-Offset).
  • Operation content-type applied after caller/default headers so it can't be clobbered.
  • Streaming bodies wired via addStream so a source error is catchable.
  • Query parameters typed by their schema (e.g. int? limit) instead of always String? — now exercised by PostgREST.
  • In-process formatting via DartFormatter (the emitter no longer shells out to dart format with an unchecked exit code).

Out of scope

Auth is not yet in sdk#51; Realtime is incompatible with REST codegen; wiring the generated clients into the real storage_client/functions_client/postgrest packages is deferred to keep the spike self-contained.

spydon added 8 commits July 1, 2026 11:22
Explores generating the supabase-dart HTTP layer (Storage + Functions)
from the shared Smithy models in supabase/sdk#51.

Adds a small custom Dart emitter that consumes the committed OpenAPI
artifacts and produces http-based clients with streaming upload,
streaming response, multipart and per-request header injection. Off-the
-shelf generators (dart-dio, dart) were rejected for lacking streaming
and requiring build_runner.

Part of SDK-1106.
Rename the package from supabase_codegen_spike, move it under packages/,
add analysis_options, align README tables and drop em dashes.
- percent-encode path parameters (wildcards keep slashes), mirroring the
  storage client fix in #1479
- parse numeric response headers tolerantly instead of force-unwrapping
- apply operation content-type after caller/default headers so it wins
- wire streaming bodies via addStream so source errors are catchable
- add tests for each fix
- rewrite README to describe the package; drop spike references from code
…ranch

The SDK branch (supabase/sdk#51) now includes a DatabaseService model.
Generate a DatabaseApi from it to verify the emitter works against every
model in the branch. Type query parameters by their schema instead of
always String?, and add PostgREST transport coverage.
Replace hand-rolled StringBuffer scaffolding with package:code_builder for
class/field/constructor/method structure and format in-process with
package:dart_style, dropping the Process.runSync('dart format') call and the
manual dart:convert import heuristic. Generated output is equivalent and all
tests pass.
- exclude the tooling package from the SDK capability-matrix scan via the
  existing .sdk-parse-ignore mechanism (it is not part of the SDK surface)
- model HTTP methods as an HttpMethod enum used via .name
- spell out abbreviated identifiers in the generator
@spydon spydon changed the title spike(codegen): Smithy/OpenAPI to idiomatic Dart HTTP client emitter chore(codegen): add openapi-to-dart http client generator spike Jul 1, 2026
spydon added 3 commits July 1, 2026 15:48
The generator needs a newer Dart than the oldest supported Flutter (its
dart_style/code_builder deps), so bootstrapping it on Flutter 3.22 fails.
Exclude it from the melos workspace like examples/launcher, and drop its
analysis_options so the repo-wide DCM scan skips it (it is not bootstrapped).

Also drop a redundant inferrable type argument in a storage_client test that
the current DCM ruleset flags (pre-existing, unrelated to this spike).
@spydon spydon changed the title chore(codegen): add openapi-to-dart http client generator spike chore(codegen): add openapi-to-dart http client generator Jul 2, 2026
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