Skip to content

feat(storage): OpenAPI codegen tool + generated Storage HTTP client#1099

Draft
grdsdev wants to merge 44 commits into
mainfrom
claude/sad-poincare-c48d88
Draft

feat(storage): OpenAPI codegen tool + generated Storage HTTP client#1099
grdsdev wants to merge 44 commits into
mainfrom
claude/sad-poincare-c48d88

Conversation

@grdsdev

@grdsdev grdsdev commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a generic OpenAPI-to-Swift code generator (tools/openapi-codegen, standalone SPM package, depends on OpenAPIKit30 and swift-argument-parser) that parses an OpenAPI 3.0.3 document into an internal IR and emits Swift models + a client targeting a new zero-dependency HTTPRuntime (copied from a prior spike into Sources/HTTPRuntime).
  • Commits Storage's OpenAPI spec (openapi/storage.json, fixed for SDK generation by supabase/storage#1215) and the client generated from it (Sources/StorageOpenAPI/) — not yet wired into the public Storage API. This proves the generator works end-to-end against a real spec; reconciling naming/shape with the hand-written StorageFileApi/StorageBucketApi and actually adopting the generated client is future work.
  • Design doc: docs/superpowers/specs/2026-07-08-storage-openapi-codegen-design.md. Implementation plan: docs/superpowers/plans/2026-07-08-storage-openapi-codegen.md.

The original plan had 15 tasks. Running the generator against Storage's real spec repeatedly surfaced constructs the plan didn't anticipate — each was checked with the user before extending the generator's scope. That produced follow-up tasks hoisting inline enums and inline objects (in schema properties, parameters, request/response bodies, and array items) into named Swift types, supporting oneOf/anyOf unions as tagged enums with hand-rolled Codable, treating typeless "fragment" schemas as freeform, skipping operations without an operationId, and fixing two non-determinism bugs plus a naming bug and a missing Xcode scheme entry along the way. The generator now produces the full real spec deterministically: 71 schemas, 60 operations.

A final whole-branch review (and further follow-ups) found and fixed: a third non-deterministic iteration (multipart fields), a broken emission path for array-typed query parameters, an emitter crash for union-typed parameters (now rejected at parse time — no well-defined query/header wire representation), consolidated the inline-object hoisting logic that had been hand-copied across four call sites into one shared helper, and replaced the CLI's hand-rolled argument parsing with swift-argument-parser.

Test plan

  • tools/openapi-codegen's own test suite: 51 tests, 10 suites, all passing
  • Full xcodebuild test suite for the main package (PLATFORM=IOS XCODEBUILD_ARGUMENT=test ./scripts/xcodebuild.sh), confirmed HTTPRuntimeTests/StorageOpenAPITests actually execute (not just present in Package.swift), zero regressions in Auth/Functions/Helpers/PostgREST/Realtime/Storage/Supabase
  • Generation is reproducible: regenerating from openapi/storage.json with the committed CLI produces byte-identical output to what's committed
  • Sources/StorageFileApi.swift/StorageBucketApi.swift/Types.swift/SupabaseStorage.swift confirmed untouched — the generated client is additive only
  • Independent per-task code review (32 tasks) plus a final whole-branch review — verdict: ready to merge with fixes, all fixes applied
  • Follow-up filed (not blocking this PR): close the remaining stringConversionExpression gap for arrays of hoisted (non-scalar) types in parameter position — not exercised by Storage's current spec

grdsdev added 30 commits July 8, 2026 12:07
Adds parseResponses/parseResponseBody (OpenAPI response -> IRResponse,
skipping default/range status codes) and parseOperations (walks every
path/method into IROperation). parseDocument ties schema and operation
parsing together into the single IRDocument entry point the emitter
and CLI will consume.
Adds SwiftEmitter.emitClient(_:clientName:), which turns IRDocument
operations into a Swift client struct with one async throws method per
operation: HTTPRequestBuilder construction with path/query/header
parameters, JSON and multipart request bodies, JSON and binary response
bodies, and typed per-status-code error decoding via
HTTPResponse.checkStatus(errorTypes:).
Reads a spec file, runs it through OpenAPIParsing.parseDocument and
SwiftEmitter.emitModels/emitClient, and writes Models.swift and
<Module>Client.swift to the output directory.

Marks OpenAPIParsing, SwiftEmitter, and the IR types public so the
executable target (a separate module from OpenAPICodegenCore) can
call them.
Replace multiple .contains(...) substring checks in ModelEmitterTests,
ClientEmitterTests, and EndToEndTests with assertInlineSnapshot, so a
single snapshot catches unintended changes anywhere in the generated
Swift output instead of only the specific substrings a .contains check
happened to look for.
…laky snapshot tests

parseObjectProperties iterated objectContext.properties (an OpenAPIKit
OrderedDictionary populated via JSONDecoder's container.allKeys) without
sorting. Decodable's keyed-container key order isn't guaranteed stable
across decode calls, so the emitted struct's property/CodingKeys order
could differ run to run, causing EndToEndTests to flake (confirmed: 1
failure in 5 consecutive runs, id/public swapped). Sort by property name
before iterating, matching the sort-for-determinism pattern already used
elsewhere in this parser (parseNamedSchema/parseDocument's schema sort,
parseOperations's operation sort, parseResponses's status-code sort).
GET /metrics (Storage's Prometheus scrape endpoint) has no operationId.
It isn't part of the typed API surface anyone calls from Swift, so skip
such operations rather than failing the whole parse.
Runs tools/openapi-codegen against openapi/storage.json and wires the
output into the main package as an internal StorageOpenAPI target
(not yet a public product, not yet consumed by Storage).

Fixes surfaced by the first real build of generated output:
- SwiftEmitter didn't mark import Foundation/HTTPRuntime as `public
  import`, so InternalImportsByDefault rejected the public APIError
  conformance and the client's public URL-typed init.
- stringConversionExpression used String.init for enum-backed query/
  header params, which is ambiguous for RawRepresentable<String>
  types; now emits .rawValue for schema-ref parameters.
- errorTypesLiteral emitted `[]` for operations with no error
  responses, which Swift can't infer as [Int: any APIError.Type];
  now emits `[:]`.
- HTTPRuntime.HTTPMethod was missing .options/.trace, needed by
  Storage's tusOptions* operations.

Also tightens two ExistentialAny warnings in emitted Codable
conformances (any Decoder/any Encoder) and adds emitter test coverage
for the enum-backed query parameter case.
Adds tests for StorageOpenAPIClient using a fake HTTPTransport: JSON
success decoding (bucketGet), typed-error decoding via the real 403
error mapping (ErrorSchema), unmapped-status fallback (HTTPError), and
the current bodyless behavior of objectUpload (multipart/file request
bodies are not wired up yet in the generated client).
@coveralls

coveralls commented Jul 9, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29018885975

Coverage decreased (-5.2%) to 77.87%

Details

  • Coverage decreased (-5.2%) from the base build.
  • Patch coverage: 805 uncovered changes across 13 files (428 of 1233 lines covered, 34.71%).
  • 6 coverage regressions across 2 files.

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
Sources/Storage/Generated/StorageGeneratedClient.swift 546 30 5.49%
Sources/HTTPRuntime/MultipartFormData.swift 191 117 61.26%
Sources/HTTPRuntime/URLSessionTransport.swift 115 41 35.65%
Sources/Storage/Generated/Models.swift 44 0 0.0%
Sources/Storage/StorageApi.swift 62 32 51.61%
Sources/HTTPRuntime/HTTPRequest.swift 48 29 60.42%
Sources/HTTPRuntime/JSONCoding.swift 36 24 66.67%
Sources/HTTPRuntime/HTTPResponse.swift 19 9 47.37%
Sources/HTTPRuntime/TransferProgress.swift 8 0 0.0%
Sources/HTTPRuntime/ServerSentEvents.swift 70 64 91.43%
Total (18 files) 1233 428 34.71%

Coverage Regressions

6 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
Sources/Auth/Internal/Keychain.swift 5 37.23%
Sources/Auth/Storage/KeychainLocalStorage.swift 1 41.67%

Coverage Stats

Coverage Status
Relevant Lines: 10687
Covered Lines: 8322
Line Coverage: 77.87%
Coverage Strength: 33.72 hits per line

💛 - Coveralls

grdsdev added 11 commits July 8, 2026 21:30
Consolidates the inline-object-with-properties hoisting check that was
hand-copied at three call sites (parseObjectProperties, parseRequestBody,
parseResponseBody) into a single hoistInlineObjectIfPresent helper,
matching the pattern already used for hoistUnionIfPresent and
hoistArrayOfObjectIfPresent. parseParameter is intentionally left
untouched since it doesn't support inline-object hoisting.
ArgumentParser's default kebab-casing of "OpenAPICodegen" produces
"open-api-codegen", which doesn't match the actual executable name and
would mislead anyone who copies the --help/error text verbatim.
…e deleted SSE parser after the package-scoping edit

The manual public->package access-control pass left HTTPRuntime non-compiling:
checkStatus still referenced HTTPResponse's now-removed bare status/isSuccess
(now only on .head), MultipartFormData's new builder API members defaulted to
internal instead of package, and ServerSentEvents.swift was deleted outright
rather than rescoped, breaking SSE stream parsing entirely.
…ipartFormData API

tools/openapi-codegen's emitter still hardcoded `public` on every generated
declaration and emitted the old MultipartFormData.Part/.append API, both of
which no longer compile against HTTPRuntime's package-scoped types and
rewritten builder-style MultipartFormData. Threads a new AccessLevel enum
(default .internal) through every emitter helper and adds a --access-level
CLI option; multipart request bodies now build via the .addFile/.addText
fluent chain. Regenerates Sources/StorageOpenAPI with the new internal
default.
…/Generated

Drop the separate StorageOpenAPI SPM target and generate the client
directly into Sources/Storage/Generated with internal access control,
so the generated types share a module with the hand-written Storage
API. Fixes a same-module HTTPRuntime/Helpers type name collision
(HTTPRequest/HTTPResponse/HTTPError) surfaced by the move in the 3
relocated test files by qualifying with the HTTPRuntime module name.
Starts wiring StorageBucketApi's read methods to the generated
StorageGeneratedClient instead of hand-rolled requests, with a
backward-compatible error translation layer (ErrorSchema/HTTPError ->
StorageError).

Fixes along the way:
- StorageApi's generatedClient transport now merges this instance's
  dynamic headers (apikey/Authorization/custom setHeader values) into
  every request via a new HeaderInjectingTransport wrapper - the
  generated client builds requests with empty headers otherwise,
  which was sending unauthenticated requests.
- Updated the remaining StorageHTTPSession(fetch:upload:) call sites
  (tests) for the uploadFromFile/bytes parameters added earlier.
- Renamed StorageOpenAPIClient -> StorageGeneratedClient in the 3
  moved test files to match the regenerated client's actual name.
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

⚠️ Potential Breaking API Changes Detected

This PR appears to contain breaking API changes. Please review the changes below:

API Check Output
  💔 API breakage: constructor StorageHTTPSession.init(fetch:upload:) has been removed

If this is intentional, please update your PR title or commit message to include:

  • ! after the type (e.g., feat!: remove deprecated method)
  • Or include BREAKING CHANGE: in the commit body

If this is a false positive, you can safely ignore this warning.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants