Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4cef9f8
docs: add SDK code generation design spec
grdsdev Jun 16, 2026
2eb128c
docs: add SDK codegen foundation implementation plan
grdsdev Jun 16, 2026
aada8e3
feat: add optional binding field to capability features
grdsdev Jun 16, 2026
329b864
feat: add codegen.yaml schema, types, and loader
grdsdev Jun 16, 2026
fe6f83e
feat: validate feature bindings against codegen config
grdsdev Jun 16, 2026
39ea8ba
feat: add openapi-generator argument builder
grdsdev Jun 16, 2026
9953257
feat: add conformance vector format and validator
grdsdev Jun 16, 2026
78fb687
fix: narrow conformance traversal error handling to ENOENT; pin recur…
grdsdev Jun 16, 2026
14f57de
feat: enforce codegen bindings and conformance vectors in validate
grdsdev Jun 16, 2026
25b7acc
refactor: clarify codegen wiring and add clean-path test
grdsdev Jun 16, 2026
1c1c090
docs: document the code generation contract
grdsdev Jun 16, 2026
b0c6cf0
docs: add Swift Storage codegen spike findings
grdsdev Jun 16, 2026
211fa73
docs: add Swift Storage pilot plan (Plan 2) and spec-normalization de…
grdsdev Jun 16, 2026
9c16041
feat: make codegen templates optional and add generate targets
grdsdev Jun 17, 2026
986608e
refactor: document target config mapping and require non-empty targets
grdsdev Jun 17, 2026
3ce906f
feat: add OpenAPI spec normalizer transforms
grdsdev Jun 17, 2026
f9b79ad
refactor: remove dead camelCase helper from normalizer
grdsdev Jun 17, 2026
c0ec6d3
fix: skip identity renames in renameSchemas
grdsdev Jun 17, 2026
1c74586
feat: normalize and commit the Storage OpenAPI spec
grdsdev Jun 17, 2026
0580d30
fix: correct bucket override keys and fail loudly on unmatched overrides
grdsdev Jun 17, 2026
5322e30
feat: add codegen.yaml for the Swift Storage pilot
grdsdev Jun 17, 2026
39a3bcb
feat: add openapi-generator runner and generate CLI
grdsdev Jun 17, 2026
af3d759
feat: generate and commit the Swift Storage core (builds clean)
grdsdev Jun 17, 2026
8fd7bc3
docs: document inlineExternalRefs stopgap and dedup nullable-keep beh…
grdsdev Jun 17, 2026
38834e1
feat: add generate:check drift guard
grdsdev Jun 17, 2026
8309139
feat: cross-check binding operationIds and bind the Storage pilot subset
grdsdev Jun 17, 2026
7a72811
fix: report a single error when a binding spec file is unreadable
grdsdev Jun 17, 2026
f19aabb
chore: ignore stray openapitools.json (pipeline uses the standalone g…
grdsdev Jun 17, 2026
297749c
docs: track upstream Storage OpenAPI spec issues and their normalizer…
grdsdev Jun 17, 2026
57043f3
docs: track missing request bodies on Storage binary upload/download …
grdsdev Jun 17, 2026
b483b22
feat: inject octet-stream body for object upload ops so uploadObject …
grdsdev Jun 17, 2026
0bdb980
docs: design lean Swift templates over a hand-written runtime
grdsdev Jun 17, 2026
0f9d47a
docs: add SupabaseRuntime implementation plan (Plan A)
grdsdev Jun 17, 2026
0840fb6
feat(runtime): scaffold SupabaseRuntime with request value types
grdsdev Jun 17, 2026
8482d69
chore(runtime): add .gitignore to exclude Swift build artifacts
grdsdev Jun 17, 2026
68edb29
fix(runtime): uppercase HTTPMethod raw values; cache path-encoder cha…
grdsdev Jun 17, 2026
87024f0
feat(runtime): add TransferProgress, TransferTask, ResponseStream
grdsdev Jun 17, 2026
1e7ca73
chore(runtime): drop unused import and pin zero-total fraction
grdsdev Jun 17, 2026
f34af7f
feat(runtime): add Transport protocol and MockTransport
grdsdev Jun 17, 2026
0d50fb3
feat(runtime): make MockTransport body-aware; public properties; more…
grdsdev Jun 17, 2026
0c96ce0
feat(runtime): add ClientConfiguration and AuthProvider
grdsdev Jun 17, 2026
89693cc
feat(runtime): URLSessionTransport send over native async URLSession
grdsdev Jun 17, 2026
6009241
fix(runtime): guard trailing-slash baseURL; test body + no-content send
grdsdev Jun 17, 2026
1b79166
feat(runtime): streaming upload/download with progress via SessionDel…
grdsdev Jun 17, 2026
633f32a
fix(runtime): emit upload/download progress via per-call task delegate
grdsdev Jun 17, 2026
b730443
test(runtime): make StubURLProtocol per-session to fix cross-suite race
grdsdev Jun 17, 2026
19cd7b4
feat(runtime): stream() for event-stream responses
grdsdev Jun 17, 2026
95b2fde
test(runtime): tighten stream non-2xx assertions and cover errorMapper
grdsdev Jun 17, 2026
433a571
feat(runtime): background session guard and relaunch hook
grdsdev Jun 17, 2026
98179d4
chore: drop stock-generated Storage package; treat generated output a…
grdsdev Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ site/

# AI-assisted planning docs (agentic skill framework artefacts — not project documentation)
docs/superpowers/

# Generated SDK output is a build product (regenerated from the committed normalized specs).
# Not committed for now — Plan B will settle the committed-vs-buildtime model for the lean templates.
codegen/generated/

# Stray @openapitools/openapi-generator-cli config (we use the standalone binary, version pinned in codegen.yaml)
openapitools.json
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,50 @@ scripts/ # TypeScript validator + site generator (scripts/capability-matr

The full schema lives in `schema/capability-matrix.schema.json`.

## Code generation contract

SDKs generate their transport, models, and error types from upstream OpenAPI specs. This repo is the contract:

- A feature may declare an optional `binding` to the OpenAPI operation it maps to:

```yaml
- id: storage.objects.upload
name: Upload Object
description: Upload a file to a bucket.
group: objects
binding:
spec: storage # must match a spec id in codegen.yaml
operationId: uploadObject
```

- `codegen.yaml` (repo root) pins the generator engine, the spec sources, and the per-language template packs:

```yaml
engine:
tool: openapi-generator
version: 7.10.0
specs:
storage:
source: https://.../storage/openapi.yaml
version: <pin>
languages:
swift:
generator: swift5
templates: templates/swift
```

- `conformance/**/*.yaml` holds language-agnostic test vectors each SDK runs:

```yaml
feature: storage.objects.upload
cases:
- name: uploads a small file
input: { path: a.txt, body: hi }
expected: { status: 200 }
```

`npm run validate` enforces that bindings reference declared specs, that `codegen.yaml` matches its schema, and that conformance vectors are well-formed and reference real features. The full schemas live in `schema/codegen.schema.json` and `schema/conformance.schema.json`.

## SDK compliance

SDK compliance is **declared in each SDK repo**, not here. To report which features your SDK implements, add a `sdk-compliance.yaml` file to the root of your SDK repo:
Expand Down
15 changes: 15 additions & 0 deletions capabilities/storage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ features:
name: Create File Bucket
description: Create a new file storage bucket.
group: file_buckets
binding:
spec: storage
operationId: createBucket
- id: storage.file_buckets.create_signed_upload_url
name: Create Signed Upload URL
description: Create a signed URL that allows an unauthenticated client to upload a file to the bucket.
Expand All @@ -38,6 +41,9 @@ features:
name: Delete File Bucket
description: Delete an existing file bucket. The bucket must be empty before deletion.
group: file_buckets
binding:
spec: storage
operationId: deleteBucket
- id: storage.file_buckets.download
name: Download File
description: Download a file from a private bucket.
Expand All @@ -58,6 +64,9 @@ features:
name: Get Bucket
description: Retrieve the details of a specific storage bucket.
group: file_buckets
binding:
spec: storage
operationId: getBucket
- id: storage.file_buckets.get_public_url
name: Get Public URL
description: Get the public URL for a file in a public bucket.
Expand All @@ -66,6 +75,9 @@ features:
name: List File Buckets
description: List all file storage buckets in the project.
group: file_buckets
binding:
spec: storage
operationId: listBuckets
- id: storage.file_buckets.list_files
name: List Files
description: List files and folders within a path of the bucket.
Expand Down Expand Up @@ -94,6 +106,9 @@ features:
name: Upload File
description: Upload a file to an existing bucket.
group: file_buckets
binding:
spec: storage
operationId: uploadObject
- id: storage.file_buckets.upload_with_signed_url
name: Upload with Signed URL
description: Upload a file using a pre-signed upload URL, without requiring standard authentication.
Expand Down
24 changes: 24 additions & 0 deletions codegen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# yaml-language-server: $schema=./schema/codegen.schema.json
engine:
tool: openapi-generator
version: "7.23.0"

specs:
storage:
source: codegen/specs/storage.normalized.json
version: "gh-pages@53e6a743d5b02e7e7e7b7549f7490517773be016"

languages:
swift:
generator: swift6
generatorProperties:
projectName: SupabaseStorage
responseAs: AsyncAwait
library: urlsession
useSPMFileStructure: "true"
nonPublicApi: "true"

targets:
- spec: storage
language: swift
output: codegen/generated/swift-storage
17 changes: 17 additions & 0 deletions codegen/normalize/storage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"schemaRenames": {
"def-0": "AuthHeader",
"def-1": "ErrorBody"
},
"operationIdOverrides": {
"POST /object/{bucketName}/{objectPath}": "uploadObject",
"GET /bucket/": "listBuckets",
"POST /bucket/": "createBucket",
"DELETE /bucket/{bucketId}": "deleteBucket",
"GET /bucket/{bucketId}": "getBucket"
},
"requestBodyInjections": {
"POST /object/{bucketName}/{objectPath}": { "required": true, "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } },
"PUT /object/{bucketName}/{objectPath}": { "required": true, "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } }
}
}
3 changes: 3 additions & 0 deletions codegen/runtime/swift/SupabaseRuntime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.build/
*.o
*.d
19 changes: 19 additions & 0 deletions codegen/runtime/swift/SupabaseRuntime/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "SupabaseRuntime",
platforms: [.macOS(.v12), .iOS(.v15), .tvOS(.v15), .watchOS(.v8)],
products: [.library(name: "SupabaseRuntime", targets: ["SupabaseRuntime"])],
targets: [
.target(
name: "SupabaseRuntime",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "SupabaseRuntimeTests",
dependencies: ["SupabaseRuntime"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/// Supplies auth headers per request; async so token refresh fits.
public struct AuthProvider: Sendable {
private let provider: @Sendable () async throws -> [String: String]
public init(_ provider: @escaping @Sendable () async throws -> [String: String]) { self.provider = provider }
public func headers() async throws -> [String: String] { try await provider() }

/// No auth headers.
public static let none = AuthProvider { [:] }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public enum SessionKind: Sendable, Equatable {
case foreground
case background(identifier: String)
}

public struct ClientConfiguration: Sendable {
public var baseURL: URL
public var defaultHeaders: [String: String]
public var auth: AuthProvider
public var encoder: JSONEncoder
public var decoder: JSONDecoder
public var sessionKind: SessionKind
/// Maps a non-2xx (body, head) to a typed error; returns nil to fall back to `TransportError.http`.
public var errorMapper: @Sendable (Data, HTTPResponseHead) -> (any Error)?

public init(
baseURL: URL,
defaultHeaders: [String: String] = [:],
auth: AuthProvider = .none,
encoder: JSONEncoder = JSONEncoder(),
decoder: JSONDecoder = JSONDecoder(),
sessionKind: SessionKind = .foreground,
errorMapper: @escaping @Sendable (Data, HTTPResponseHead) -> (any Error)? = { _, _ in nil }
) {
self.baseURL = baseURL
self.defaultHeaders = defaultHeaders
self.auth = auth
self.encoder = encoder
self.decoder = decoder
self.sessionKind = sessionKind
self.errorMapper = errorMapper
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

public enum HTTPMethod: String, Sendable {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
case head = "HEAD"
}

public struct HTTPRequest: Sendable {
public var method: HTTPMethod
public var path: String
public var query: [URLQueryItem]
public var headers: [String: String]

public init(method: HTTPMethod, path: String, query: [URLQueryItem] = [], headers: [String: String] = [:]) {
self.method = method
self.path = path
self.query = query
self.headers = headers
}

public init(method: HTTPMethod, path: RequestPath, query: [URLQueryItem] = [], headers: [String: String] = [:]) {
self.init(method: method, path: path.value, query: query, headers: headers)
}
}

public struct HTTPResponseHead: Sendable {
public let status: Int
public let headers: [String: String]
public init(status: Int, headers: [String: String]) {
self.status = status
self.headers = headers
}
}

public enum UploadSource: Sendable {
case file(URL)
case data(Data)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

/// In-memory `Transport` for tests. The `responder` receives the request and the
/// encoded request body (nil for body-less sends/downloads), so tests can assert both.
public struct MockTransport: Transport {
public typealias Responder = @Sendable (HTTPRequest, Data?) async throws -> (Int, Data)
public let responder: Responder
public let encoder: JSONEncoder
public let decoder: JSONDecoder

public init(encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), _ responder: @escaping Responder) {
self.responder = responder
self.encoder = encoder
self.decoder = decoder
}

public func send<R: Decodable & Sendable>(_ request: HTTPRequest) async throws -> R {
let (_, data) = try await responder(request, nil)
return try decoder.decode(R.self, from: data)
}

public func send<B: Encodable & Sendable, R: Decodable & Sendable>(_ request: HTTPRequest, body: B) async throws -> R {
let bodyData = try encoder.encode(body)
let (_, data) = try await responder(request, bodyData)
return try decoder.decode(R.self, from: data)
}

public func send(_ request: HTTPRequest) async throws {
_ = try await responder(request, nil)
}

public func upload<R: Decodable & Sendable>(_ request: HTTPRequest, from source: UploadSource) -> TransferTask<R> {
let responder = self.responder
let decoder = self.decoder
let (stream, cont) = AsyncStream<TransferProgress>.makeStream()
cont.finish()
return TransferTask(
progress: stream,
value: {
let bodyData: Data?
switch source {
case .data(let d): bodyData = d
case .file(let url): bodyData = try Data(contentsOf: url)
}
let (_, data) = try await responder(request, bodyData)
return try decoder.decode(R.self, from: data)
},
cancel: {}
)
}

public func download(_ request: HTTPRequest, toFile destination: URL) -> TransferTask<Void> {
let responder = self.responder
let (stream, cont) = AsyncStream<TransferProgress>.makeStream()
cont.finish()
return TransferTask(
progress: stream,
value: { let (_, data) = try await responder(request, nil); try data.write(to: destination) },
cancel: {}
)
}

public func stream(_ request: HTTPRequest) async throws -> ResponseStream {
let (status, data) = try await responder(request, nil)
let body = AsyncThrowingStream<ArraySlice<UInt8>, any Error> { cont in
cont.yield(ArraySlice(data))
cont.finish()
}
return ResponseStream(head: HTTPResponseHead(status: status, headers: [:]), body: body)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

private let pathParamAllowedCharacters: CharacterSet = {
var cs = CharacterSet.urlPathAllowed
cs.remove("/")
return cs
}()

public struct RequestPath: Sendable, ExpressibleByStringInterpolation {
public let value: String

public init(stringLiteral value: String) { self.value = value }
public init(stringInterpolation: StringInterpolation) { self.value = stringInterpolation.text }
public init(_ path: RequestPath) { self.value = path.value }

public struct StringInterpolation: StringInterpolationProtocol {
var text = ""
public init(literalCapacity: Int, interpolationCount: Int) { text.reserveCapacity(literalCapacity) }
public mutating func appendLiteral(_ literal: String) { text += literal }
/// Percent-encodes a raw path-segment value (slashes become %2F). Pass the raw, unencoded value — do not pre-encode.
public mutating func appendInterpolation(param value: String) {
text += value.addingPercentEncoding(withAllowedCharacters: pathParamAllowedCharacters) ?? value
}
}
}
Loading