Repository navigation
Conversation
Adds an EndpointsGeneratorPlugin build tool plugin that turns
<Stem>.openapi.{yaml,yml,json} files in a target into Endpoints code at
build time, with byte-identical output for the same spec, config, stem,
and generator version.
- Swift tools 6.1. A default-on OpenAPIGenerator trait guards the exact
OpenAPIKit 6.4.0 and Yams 6.2.2 pins, so runtime-only consumers can
drop them with traits: []. With the trait off, the CLI is a stub that
says to enable it.
- EndpointsGeneratorCore: loader with 3.0 to 3.1 conversion, a closed
versioned config, IR, the frozen naming contract (collisions and
reserved names are errors, fixed with renames), Tarjan cycle analysis,
a code writer, and sorted diagnostics with source positions.
- Mapping subset: servers and environments, object schemas, required
nullable fields, open and closed enums, simple path/query/header
parameters, JSON bodies, and single success and error responses.
Constructs that need runtime hooks report as unsupported for now.
- Compiled golden tests, cross-process determinism tests, a banned-API
lint, and example packages covering the plugin end to end and the
trait-disabled runtime-only fetch.
- The design and verification records are in docs/.
String.pathSafe percent-encoded each value, and urlRequest(in:) then assigned the joined path to URLComponents.path, which encodes again. A value like "a b" went out as "a%2520b". This has been the case since the first commit; 5077dd2 fixed the same problem for query and form values in 2019, but the path side was missed because every path test used plain values. - String.pathSafe encodes a value as one segment, including "/", "?" and "#", and keeps "." and ".." as encoded segments so URL resolution doesn't collapse them. - PathTemplate passes every component through a normalizer that keeps valid %XX escapes and encodes anything else not allowed in a path. The result is assigned to percentEncodedPath, so nothing is encoded twice and custom PathRepresentable values that return raw text don't trap. - Literal template text keeps its slashes and honours %XX escapes. - Form body encoding is byte-identical to before.
Runtime (additive): - DecoderType.decode(_:from:response:) with a default that forwards to decode(_:from:), and CodingUserInfoKey.httpStatusCode/.httpHeaders. - HTTPResponseJSONDecoder: a Sendable decoder that configures a fresh JSONDecoder per call, decodes an empty body as null, exposes the status and headers to Decodable types, and can decode selected statuses as Data. - Every request path forwards the HTTPURLResponse to the decoders. - MultiValueParameterRepresentable: arrays become repeated query and form items. An existing ParameterRepresentable conformance still wins. - Int32, Int64, UUID, and RawRepresentable values work as path and query parameters. Generator: - Status-tagged Response and ErrorResponse enums for operations with several outcomes, including body-less statuses, ranges, and default. - Typed path and query parameters, array query parameters, optional headers, and path-item/operation parameter merging. Three tests that depend on runtime bugs fixed separately (path double-encoding, optional headers throwing, empty error bodies) are wrapped in withKnownIssue. Also adds the SSE design addendum and a "Migrating from 0.5" README section.
…dpoint-generation-f58e25
With the path double-encoding fix merged in, a template like
/files/{id}.json, where the parameter shares a segment with literal text,
broke: the generator built the whole path as one raw String, and the
runtime now encodes a String as a single segment, so the literal "/"
went out as %2F. The raw interpolation was also a latent bug, because a
value containing "/" or "?" could add path structure.
Embedded paths now produce an internal EncodedPath: PathRepresentable
assembled from the literal text and each value's pathSafe, so the
separators stay and every value is encoded once. Also removes the
withKnownIssue wrapper from pathSpecialCharactersEncodeOnce, which
passes now.
includesSlash: false removed whatever slash came before the value,
including one written in the template, so
"testing/\(path: \.name, includesSlash: false).test" built
"testingfirst.test". It now removes only a slash the template inserted
automatically, which is what the 2021 failing test on feature/path-slash-fix
expected.
Default interpolations are unchanged: each value is its own segment, so
"files/\(path: \.id).json" still builds "files/42/.json". Embed a value with
includesSlash: false ("files/42.json"). The PathTemplate docs and
Examples.md now state the rule.
…dpoint-generation-f58e25
…nt-generation-f58e25
Adds EndpointsGeneratorConfig, a dependency-free library product with the public configuration API: EndpointsConfiguration (stored, defaulted settings with a labelled initializer), NamingStrategy (.verbatim, .camelCase, .upperCamelCase, .custom), NameContext, and typed name identities (NameTarget, TypeID, OperationID, ParameterLocation). Internals are package-scoped. Naming v2 treats every non-alphanumeric byte as a word separator, so IN_PROGRESS becomes inProgress, pet_record becomes PetRecord, and get_pet_by_id becomes GetPetByIdEndpoint. Acronym runs the server wrote are kept, and a digit followed by an uppercase letter and then a lowercase one starts a new word (HTTP2Status, UTF8String, 2FA -> _2fa). The reference algorithm is table-tested. Core now collects a name-independent inventory, resolves every name once, and lowers once. Renames use typed targets with parameter locations and structured owners. Ambiguous and invalid renames are errors, and unused renames are warnings. x-swift-name and x-enum-varnames are honoured. Wire names are unchanged. There is no user-facing JSON config: the Swift config file arrives in the next phase. A private --configuration-json flag feeds tests.
A target can now declare its generator settings in <Stem>.endpoints.swift:
// endpoints-configuration-version: 1
import EndpointsGeneratorConfig
let petstoreConfiguration = EndpointsConfiguration(
accessLevel: .public,
enumCaseNamingStrategy: .verbatim,
operationNamingStrategy: .custom { $0.upperCamelCased }
)
The file is an ordinary source of the target, so Xcode type-checks and
autocompletes it. At build time the generator compiles it with the
toolchain swiftc, together with an embedded copy of the config library,
into a small config program, runs it, and uses its answers for every name.
- Determinism: a lexical check before compiling rejects imports other
than EndpointsGeneratorConfig, # directives, regex literals, key paths,
randomness, and identity and address channels. The program runs with an
empty environment and deterministic hashing, and every evaluation runs
it twice in separate processes and requires identical results.
- The cache key covers the config, DSL, runner, compiler and SDK
identity. Entries are published atomically under a lock, each execution
gets its own files, and eviction is lease-safe. A cached run takes about
0.2 s.
- Diagnostics point at the user's file: compiler errors and warnings are
forwarded verbatim, entry-symbol and lexical problems are reported at
their line, and warnings don't fail generation.
- The plugin finds swiftc through the plugin context in SwiftPM and
Xcode, always compiles against the host macOS SDK (so iOS targets
work), and checks the EndpointsGeneratorConfig dependency where SwiftPM
exposes it.
- Verified on macOS (sandbox on), Linux swift:6.1, and native Xcode macOS
and iOS simulator targets. Xcode re-runs the plugin command on every
build, because of a fresh script GUID; outputs stay untouched.
Adds a Generating DocC article and a README quick start covering setup in SwiftPM and Xcode, package traits, the generated code, idiomatic naming, every EndpointsConfiguration setting with examples, the rules for configuration files, diagnostics, and current limitations. Code samples were compiled against the generator, and quoted output and messages come from real runs. Configuration checks now match nondeterministic standard-library APIs by exact name, so ordinary helpers such as randomPrefix or streetAddress are allowed. capacity is banned, since it varies with the host allocator. The two-run determinism error points at the entry declaration, and diagnostics use the configuration path as given. The design doc now notes that its mapping tables describe the full plan, and points to the guide for what is implemented today.
FileHandle.seekToEnd() and read(upToCount:) need macOS 10.15.4, which broke the macOS CI builds. Use them when available and fall back to seekToEndOfFile() and readData(ofLength:) otherwise.
Generated Petstore.Endpoints.swift and the user's Petstore.endpoints.swift differ only by case, so on a case-insensitive filesystem both compiled to the same object file and the link failed with duplicate symbols. Generated files are now <Stem>.<Part>.generated.swift. The plugin reports an error if any other file in the target collides with a generated name ignoring case, and outputs left over from the old names are removed.
SwiftPM 6.1 resolves dependencies behind disabled traits, as the design and the Generating guide note, so the Xcode 16.4 job fetched OpenAPIKit and Yams for the runtime-only example. Skip the assertion on 6.1.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an OpenAPI → Endpoints generator as a SwiftPM build tool plugin. You put
Petstore.openapi.yamlin a target, and every build generates the server, models, and endpoints for that API. Output is deterministic: the same spec, config, file stem, and generator version produce byte-identical files on any machine or OS. It also adds the small set of runtime hooks the generated code needs.Changes
Package
OpenAPIGeneratortrait guards the generator's dependencies, OpenAPIKit 6.4.0 and Yams 6.2.2, both pinned exactly. Apps that only use the runtime can passtraits: [], and on SwiftPM 6.2+ they then don't fetch either package. With the trait off, the CLI is a stub that tells you to enable it.EndpointsGeneratorPluginproduct, theEndpointsGeneratorCoretarget, theendpoints-generateCLI, and two test targets.Generator (
Sources/EndpointsGeneratorCore)Pipeline: loader (3.0 specs are converted to 3.1) → validation → IR → naming → rendering into four fixed files per spec. Diagnostics carry
file:line:coland the JSON pointer.Naming is a frozen, ASCII-only contract. Collisions and reserved names are errors that point you to a
renamesentry in the optional<Stem>.endpoints.jsonconfig. There's no auto-suffixing.Mapping covered so far:
null.ResponseandErrorResponseenums.Constructs not mapped yet (oneOf/anyOf/allOf, recursion, multipart and form bodies) report "unsupported", which is an error by default and a warning with
strict: false.Runtime hooks (
Sources/Endpoints, all additive)DecoderType.decode(_:from:response:)with a default implementation, plus theCodingUserInfoKey.httpStatusCodeand.httpHeaderskeys.HTTPResponseJSONDecoder: configures a freshJSONDecoderfor each call, decodes an empty body asnull, and can decode selected statuses asData.MultiValueParameterRepresentable: arrays are sent as repeated query or form items. If you already have your ownParameterRepresentableconformance, it still wins.Int32,Int64,UUID, andRawRepresentabletypes work as path and query parameters.Docs
docs/OpenAPIGenerator-Design.md(design and decisions) anddocs/OpenAPIGenerator-SSE.md(an accepted design for Server-Sent Events behind a default-offServerSentEventstrait, planned as a later phase).Behaviour changes for existing users
UUID,Int32, orInt64parameter conformances, delete them. Swift now warns that they're redundant, and request building uses the library's version. AUUIDis sent asuuidString(uppercase).Naming and configuration
Idiomatic names by default.
IN_PROGRESSbecomes.inProgress,pet_recordbecomesPetRecord,owner_namebecomesownerName, andget_pet_by_idbecomesGetPetByIdEndpoint. Wire names never change:CodingKeysand raw values keep the server spelling.A typed Swift configuration file,
<Stem>.endpoints.swift, in the target. It's type-checked by Xcode, and the generator evaluates it at build time with no extra steps:It supports per-category strategies (
.verbatim,.camelCase,.upperCamelCaseand.custom), typed renames with parameter locations, andx-swift-name/x-enum-varnames. Collisions and invalid names are errors, and unused renames are warnings.Determinism for configs that run code. A pre-compile check rejects extra imports,
#directives, regex literals, key paths, and nondeterministic standard-library APIs. The config program runs with an empty environment and deterministic hashing, and every evaluation runs it twice in separate processes and requires identical results.Verification: macOS with the plugin sandbox on, Linux
swift:6.1(175 tests), and native Xcode macOS and iOS simulator targets. Native Xcode re-runs the generate step on every build, because Xcode gives the plugin command a new script GUID each time. The cached run takes about 0.2 s and leaves outputs untouched.Docs: a new
GeneratingDocC article and a README quick start.Relationship to #24
The path-building fixes from #24 are now in
main. With them in place, embedded path templates such as/files/{id}.jsonencode each value as its own segment through an internalEncodedPathtype.Two tests are wrapped in
withKnownIssuefor runtime bugs that are still to be fixed separately:optionalHeadersOmitNilAndDescribePresentValues: optional header components throwinvalidHeaderinstead of being omitted.emptyBodyErrorUsesDefaultEmptyCodable: an empty error body fails to decode asEmptyCodable.Each wrapper starts failing once its fix lands, which flags it for removal.
Testing
swift build --build-tests -Xswiftc -warnings-as-errorsis clean, and all suites pass with exactly the 2 known issues. Generated code produces zero warnings.swift:6.1in Docker): builds, and 122 tests pass with the same 2 known issues.Examples/PetstoreClient(two modules, a public auth extension, a status-union endpoint) builds and tests with the plugin sandbox on, andExamples/RuntimeOnlyClient(traits: []) resolves without OpenAPIKit or Yams..xcodeprojusing the plugin.Coverage (line, macOS)
EndpointsGeneratorCore: 94.0%. The lowest file isSecurityDocumentation.swiftat 71.7%.Endpointsruntime: 76.0% overall. The new files are well covered; the low spots are older code (Server.swift10%,URLSession+Endpoints.swift45%,MultipartFormEncoder.swift47%).