Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6199367
docs: add design spec for Storage OpenAPI codegen tool
grdsdev Jul 8, 2026
8babc4f
docs: add implementation plan for Storage OpenAPI codegen
grdsdev Jul 8, 2026
ec4f24b
feat(runtime): copy zero-dependency HTTPRuntime from the codegen spike
grdsdev Jul 8, 2026
13c2c99
fix(runtime): use per-file public import instead of a target-wide fea…
grdsdev Jul 8, 2026
7d6971c
chore(codegen): scaffold the openapi-codegen tool package
grdsdev Jul 8, 2026
cbb977c
feat(codegen): parse object and string-enum schemas into IR
grdsdev Jul 8, 2026
f6d141f
test(codegen): cover array/ref/freeform/fail-fast type parsing
grdsdev Jul 8, 2026
87c2d2d
feat(codegen): parse path/query/header parameters into IR
grdsdev Jul 8, 2026
b66c51c
feat(codegen): parse JSON and multipart request bodies into IR
grdsdev Jul 8, 2026
169930d
feat(codegen): parse responses and wire up full-document parsing
grdsdev Jul 8, 2026
30bb244
feat(codegen): add Swift identifier casing/escaping helpers
grdsdev Jul 8, 2026
f8d24f7
feat(codegen): emit Swift models from IR schemas
grdsdev Jul 8, 2026
2467466
feat(codegen): emit a Swift HTTP client from IR operations
grdsdev Jul 8, 2026
c6af030
feat(codegen): wire up the openapi-codegen CLI
grdsdev Jul 8, 2026
86e0263
feat(codegen): hoist inline enum object properties into named schemas
grdsdev Jul 8, 2026
b3d5106
feat(codegen): hoist inline object request/response bodies into named…
grdsdev Jul 8, 2026
ca9d0c2
fix(codegen): use identifier-safe names for hoisted response-body sch…
grdsdev Jul 8, 2026
4703169
test(codegen): use inline snapshots for generated-code assertions
grdsdev Jul 8, 2026
cd30b43
fix(codegen): sort object properties deterministically to eliminate f…
grdsdev Jul 8, 2026
22ba344
feat(codegen): hoist inline enum parameters into named schemas
grdsdev Jul 8, 2026
4cb345f
fix(codegen): skip operations without an operationId instead of failing
grdsdev Jul 8, 2026
0e6affc
fix(codegen): sort path and schema iteration for deterministic parse …
grdsdev Jul 8, 2026
789782d
feat(codegen): support oneOf/anyOf unions as tagged Swift enums
grdsdev Jul 8, 2026
ee078a3
fix(codegen): treat typeless fragment schemas as freeform
grdsdev Jul 8, 2026
ac67f33
fix(codegen): disambiguate colliding union case names
grdsdev Jul 8, 2026
f9b42af
docs(storage): commit the OpenAPI spec fixed by storage#1215
grdsdev Jul 8, 2026
e7f2290
feat(codegen): hoist inline objects inside array items
grdsdev Jul 8, 2026
f84586f
feat(storage): generate the OpenAPI-based Storage client
grdsdev Jul 8, 2026
c80d66d
test(storage): cover the generated OpenAPI client's core paths
grdsdev Jul 8, 2026
0b399ce
chore(ci): add HTTPRuntimeTests and StorageOpenAPITests to the Supaba…
grdsdev Jul 8, 2026
340d3ec
chore(spell-check): add terms from the OpenAPI codegen tool and gener…
grdsdev Jul 8, 2026
51dc415
fix(codegen): sort multipart field iteration for deterministic output
grdsdev Jul 8, 2026
3939d73
fix(codegen): correctly emit array query parameters, reject union par…
grdsdev Jul 9, 2026
b6f8c56
refactor(codegen): extract shared inline-object hoisting helper
grdsdev Jul 9, 2026
6d9e110
refactor(codegen): use swift-argument-parser for the CLI
grdsdev Jul 9, 2026
29d9e11
fix(codegen): set explicit ArgumentParser command name
grdsdev Jul 9, 2026
5200e95
style: format openapi-codegen
grdsdev Jul 9, 2026
3da0c8a
publicaly import FoundationNetworking
grdsdev Jul 9, 2026
75ed269
ci: drop PR title subject requirement
grdsdev Jul 9, 2026
52b0f6a
do not expose HTTPRuntime publically
grdsdev Jul 9, 2026
9f4f54a
fix(runtime): repair checkStatus, multipart access levels, and restor…
grdsdev Jul 9, 2026
883bab6
feat(codegen): support configurable access level, target the new Mult…
grdsdev Jul 9, 2026
78facb7
refactor(storage): move generated OpenAPI client into Sources/Storage…
grdsdev Jul 9, 2026
2bab4f7
feat(storage): wire listBuckets/getBucket to the generated client
grdsdev Jul 9, 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
6 changes: 0 additions & 6 deletions .github/workflows/conventional-commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,3 @@ jobs:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
15 changes: 14 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ let package = Package(
"Helpers",
]
),
.target(
name: "HTTPRuntime"
),
.testTarget(
name: "HTTPRuntimeTests",
dependencies: [
"HTTPRuntime"
]
),
.target(
name: "Auth",
dependencies: [
Expand Down Expand Up @@ -170,6 +179,7 @@ let package = Package(
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "HTTPTypes", package: "swift-http-types"),
"Helpers",
"HTTPRuntime",
]
),
.testTarget(
Expand All @@ -181,6 +191,7 @@ let package = Package(
"Mocker",
"TestHelpers",
"Storage",
"HTTPRuntime",
],
exclude: [
"__Snapshots__"
Expand Down Expand Up @@ -230,7 +241,9 @@ let package = Package(
// Test targets migrated to Swift Testing get full Swift 6 checking, same as
// production targets. Everything else stays pinned to v5 until its migration
// phase lands (see SDK-435).
let swift6TestTargets: Set<String> = ["SupabaseTests", "HelpersTests"]
let swift6TestTargets: Set<String> = [
"SupabaseTests", "HelpersTests", "HTTPRuntimeTests",
]

for target in package.targets {
// Test targets never opted into `ExistentialAny` below, so bumping swift-tools-version
Expand Down
43 changes: 43 additions & 0 deletions Sources/HTTPRuntime/HTTPError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// HTTPError.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
package import Foundation

/// Errors surfaced by the runtime itself (transport/encoding/decoding), as
/// distinct from typed API errors decoded from a response body.
package enum HTTPError: Error, Sendable {
case invalidURL(base: URL, path: String)
case transport(any Error)
case decoding(any Error)
case encoding(any Error)
/// A non-success status whose body did not decode to any modeled error.
case unexpectedStatus(status: Int, body: Data)
}

/// Marker protocol for generated, typed API errors decoded from a response
/// body for a known status code.
package protocol APIError: Error, Sendable, Decodable {}

extension HTTPResponse {
/// Validates the status code, decoding a modeled error when the status
/// matches one of the provided error types. Generated code passes the
/// `status -> ErrorType` table declared by the operation's Smithy/TypeSpec
/// error bindings.
package func checkStatus(errorTypes: [Int: any APIError.Type]) throws {
guard !head.isSuccess else { return }
if let errorType = errorTypes[head.status] {
do {
let decoded = try JSONCoding.decoder.decode(errorType, from: body)
throw decoded
} catch let error as any APIError {
throw error
} catch {
throw HTTPError.decoding(error)
}
}
throw HTTPError.unexpectedStatus(status: head.status, body: body)
}
}
19 changes: 19 additions & 0 deletions Sources/HTTPRuntime/HTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// HTTPMethod.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
// Part of the internal HTTP runtime. NEVER exposed as public SDK surface.

/// HTTP verbs supported by the generated operations.
package enum HTTPMethod: String, Sendable, Hashable {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case head = "HEAD"
case options = "OPTIONS"
case trace = "TRACE"
}
99 changes: 99 additions & 0 deletions Sources/HTTPRuntime/HTTPRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// HTTPRequest.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
package import Foundation

/// The body of an outgoing request.
///
/// `.file` is the key to streaming large uploads without loading them into
/// memory: `URLSession` streams the file from disk. `.multipart` assembles a
/// `multipart/form-data` body onto a temporary file and then uploads that file,
/// so even large multipart parts never materialize fully in memory.
package enum HTTPBody: Sendable {
case data(Data)
case file(URL)
case multipart(MultipartFormData)
}

/// A fully-resolved HTTP request: absolute URL, headers, and body.
package struct HTTPRequest: Sendable {
package var method: HTTPMethod
package var url: URL
package var headers: [String: String]
package var body: HTTPBody?

package init(
method: HTTPMethod,
url: URL,
headers: [String: String] = [:],
body: HTTPBody? = nil
) {
self.method = method
self.url = url
self.headers = headers
self.body = body
}
}

/// Assembles an `HTTPRequest` from a base URL, a path template already filled
/// with path parameters, repeated query items, and headers.
///
/// Generated code drives this builder; it never constructs `URLComponents`
/// directly. Query values use repeated-key encoding (`?k=a&k=b`) to match the
/// Smithy/OpenAPI list conventions in the specs.
package struct HTTPRequestBuilder: Sendable {
private let method: HTTPMethod
private let baseURL: URL
private let path: String
private var queryItems: [URLQueryItem] = []
private var headers: [String: String] = [:]
private var body: HTTPBody? = nil

package init(method: HTTPMethod, baseURL: URL, path: String) {
self.method = method
self.baseURL = baseURL
self.path = path
}

package mutating func addQuery(_ name: String, _ value: String?) {
guard let value else { return }
queryItems.append(URLQueryItem(name: name, value: value))
}

package mutating func addQuery(_ name: String, _ values: [String]?) {
guard let values else { return }
for value in values {
queryItems.append(URLQueryItem(name: name, value: value))
}
}

package mutating func setHeader(_ name: String, _ value: String?) {
guard let value else { return }
headers[name] = value
}

package mutating func setBody(_ body: HTTPBody?) {
self.body = body
}

package func build() throws -> HTTPRequest {
// Compose by string so slashes inside greedy path params ({path+}) are
// preserved. Generated code percent-encodes individual label values.
var base = baseURL.absoluteString
if base.hasSuffix("/") { base.removeLast() }
let prefixedPath = path.hasPrefix("/") ? path : "/" + path
guard var components = URLComponents(string: base + prefixedPath) else {
throw HTTPError.invalidURL(base: baseURL, path: path)
}
if !queryItems.isEmpty {
components.queryItems = queryItems
}
guard let url = components.url else {
throw HTTPError.invalidURL(base: baseURL, path: path)
}
return HTTPRequest(method: method, url: url, headers: headers, body: body)
}
}
54 changes: 54 additions & 0 deletions Sources/HTTPRuntime/HTTPResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// HTTPResponse.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
package import Foundation

/// The status line and headers of a response, without the body.
///
/// Used on its own for streaming responses (where the body is delivered
/// separately as an `AsyncSequence`) and embedded in ``HTTPResponse`` for
/// buffered responses.
package struct HTTPResponseHead: Sendable {
package let status: Int
package let headers: [String: String]

package init(status: Int, headers: [String: String]) {
self.status = status
self.headers = headers
}

package func header(_ name: String) -> String? {
// HTTP header names are case-insensitive.
if let exact = headers[name] { return exact }
let lowered = name.lowercased()
return headers.first { $0.key.lowercased() == lowered }?.value
}

package var isSuccess: Bool { (200..<300).contains(status) }
}

/// A fully-buffered response.
package struct HTTPResponse: Sendable {
package let head: HTTPResponseHead
package let body: Data

package init(head: HTTPResponseHead, body: Data) {
self.head = head
self.body = body
}
}

/// A streaming response: the head arrives first, the body is an async sequence
/// of `Data` chunks (used for large downloads and event streams).
package struct HTTPResponseStream: Sendable {
package let head: HTTPResponseHead
package let body: AsyncThrowingStream<Data, any Error>

package init(head: HTTPResponseHead, body: AsyncThrowingStream<Data, any Error>) {
self.head = head
self.body = body
}
}
23 changes: 23 additions & 0 deletions Sources/HTTPRuntime/HTTPTransport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// HTTPTransport.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
/// The abstraction generated clients depend on. Kept deliberately small so the
/// generated code never touches `URLSession` directly and so tests can inject a
/// mock transport.
package protocol HTTPTransport: Sendable {
/// Buffered request/response.
func send(_ request: HTTPRequest, uploadProgress: ProgressHandler?) async throws -> HTTPResponse

/// Streaming response: head first, body as an async sequence of chunks.
/// Used for large downloads and event streams.
func stream(_ request: HTTPRequest) async throws -> HTTPResponseStream
}

extension HTTPTransport {
package func send(_ request: HTTPRequest) async throws -> HTTPResponse {
try await send(request, uploadProgress: nil)
}
}
53 changes: 53 additions & 0 deletions Sources/HTTPRuntime/JSONCoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// JSONCoding.swift
// HTTPRuntime
//
// Created by Guilherme Souza on 08/07/26.
//
package import Foundation

/// Shared JSON coders used by generated code. Dates are ISO-8601 with
/// fractional seconds, matching the mock server and the spec timestamps.
package enum JSONCoding {
package static let encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom { date, enc in
var container = enc.singleValueContainer()
try container.encode(iso8601.string(from: date))
}
return encoder
}()

package static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { dec in
let container = try dec.singleValueContainer()
let string = try container.decode(String.self)
guard let date = iso8601.date(from: string) ?? iso8601NoFraction.date(from: string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid ISO-8601 date: \(string)"
)
}
return date
}
return decoder
}()

/// ISO-8601 string with fractional seconds, for `@httpQuery` timestamp params.
package static func iso8601String(_ date: Date) -> String {
iso8601.string(from: date)
}

nonisolated(unsafe) private static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()

nonisolated(unsafe) private static let iso8601NoFraction: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()
}
Loading
Loading