Swift framework for generated REST clients.
Sunday is delivered as a Swift package using the Swift Package Manager.
SPM dependency declaration:
.package(name: "Sunday", url: "https://github.com/outfoxx/sunday-swift.git", from: <version>),Generated clients select operation security from an explicitly selected profile. Register
providers under the names in those bindings and pass the manager to URLSessionTransport.
Secrets, interactive authorization, and token persistence remain application responsibilities.
let provider = try URLSessionOAuthTokenProvider(configuration: .init(
identity: "external-service", clientID: applicationClientID,
clientSecret: applicationClientSecret, authentication: .clientSecretBasic
))
let tokens = try TokenManager(providers: ["external": provider])
let transport = URLSessionTransport(baseURL: "https://api.example.com", tokenManager: tokens)The native provider supports client credentials and application-managed authorization
code/PKCE. For an interactive session, supply a distinct grantIdentity and an authorize
callback returning AuthorizationGrant with a fresh code, redirect URI, and verifier. A
session that cannot refresh throws AuthorizationRequiredError; consumed codes are never
reused. External/static credentials implement TokenProvider; providers supporting refresh
also implement RefreshingTokenProvider.
The actor-based manager partitions tokens by provider/client identity, profile, endpoints,
scopes, audience/resource, and grant identity. It coalesces renewal, applies expiry skew,
preserves rotated refresh tokens, and lets individual callers cancel without interrupting
other waiters. Canceling the last waiter cancels acquisition. Use TokenStore for custom
persistence, and call await tokens.close() when its application/session ends. Expiry uses
Date values. Endpoint overrides change acquisition without rewriting issuer trust.
Managed requests suppress native redirects and ambient authentication. One recovery is
allowed for an explicit Bearer invalid_token challenge on a bodyless GET, HEAD, or OPTIONS.
403 responses and unsafe/body-carrying requests do not replay. Event subscriptions share
one recovery budget across reconnects, and closing a subscription cancels pending acquisition.
Generated models use one hierarchy for reads, storage, editing, and subsequent requests.
Call model.isValid(.request) or try model.validate(.request) before submitting a value;
use .response for received/emitted responses. Generated type-associated validators such as
ItemValidation also expose these calls, including for aliases and collection schemas.
Request mode rejects declared enum/union fallbacks by default. Response mode retains the
schema's declared tolerance. Validation never applies defaults or changes the value, and
validity is not cached. validate collects stable reason codes and wire paths during the
same canonical check used by isValid; ModelValidationError.diagnostics provides those
failures as JSON Pointers. Shared references are allowed, while object cycles are rejected.
Generated transport hooks revalidate current values immediately before encoding on every
execution. Constructors and standalone Codable adapters use response semantics.
Copyright 2021 Outfox, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Credentials are isolated by logical security scheme as well as provider and acquisition inputs. Discovery metadata is fetched and verified on each acquisition or renewal. Temporary provider outages allow event connections to reconnect; a rejected refresh grant triggers fresh client credentials only for the client-credentials flow. Interactive sessions require fresh application authorization. Built-in OAuth providers retain at most 1,024 consumed authorization-code hashes per provider instance. After this limit, create a provider for a newly authorized application session; old hashes are never evicted to allow code reuse. Refresh exchanges do not consume this history.
parameterValidation is an optional callback on the request specification. The transport invokes it
before encoding on every request build, including bodyless requests and event streams. Generated
callbacks validate captured typed parameters in request mode; reusing an operation checks mutable
values again. Custom transports must call OperationSpec.validateParameters() before parameter encoding. Failures
use SundayError.requestEncodingFailed(.parameterValidationFailed(error:)) and stop SSE reconnection.
Use non-optional UpdateOp<Value> for members that cannot be deleted and PatchOp<Value> for
members that can. Both default to .unchanged in generated models. .set(value) supplies an update;
PatchOp.delete writes JSON null to delete the member. Requiredness determines deletion permission,
independently of value nullability. Use non-optional value types: JSON Merge Patch cannot assign a
literal null to an object member.
struct ItemPatch: Codable {
var name: UpdateOp<String> = .unchanged
var note: PatchOp<String> = .unchanged
}
var patch = ItemPatch()
patch.name = .set("Updated")
patch.note = .delete
patch.name = .unchanged // Cancel the name update.Synthesized Codable and the keyed decode/encode overloads preserve omitted, set, and deleted
states. Encoding .unchanged outside a keyed member throws, because it has no standalone JSON
representation. Encoding .set(nil) also throws; it must never silently become deletion.
use skips unchanged operations, and get returns nil for them. Legacy optional-operation
decodeIfExists/encodeIfExists helpers remain available: nil and .unchanged both omit the member,
while .delete preserves JSON null. Synthesized encoding also omits nil optional properties, but
a standalone nil optional encodes as JSON null without invoking the operation's encoder. Use
non-optional operations to retain explicit patch states.
URI.Template.complete expands missing and nil variables as undefined under RFC 6570.
For an undefined id, /items{/id} becomes /items, while /items/{id} becomes
/items/ because the literal slash remains. Empty strings remain defined: an empty
id in /items{/id} produces /items/. Explicit nil overrides a parameter
stored on the template; omitting the override keeps the stored value. This applies
to template expressions in both the base URI and the operation path.
URI-template expansion does not enforce API-required inputs. Callers must validate
any values their API requires before constructing a request. For example, omitting
env from https://{env}example.com produces https://example.com.
let settings = try ClientSettings.resolve(
baseURL: baseURL, alternatives: alternatives, credentials: credentials,
tokenManagerFactory: { providers in
try TokenManager(providers: providers, store: applicationStore, expirySkew: 30,
now: applicationClock)
}
)The same optional tokenManagerFactory: TokenManagerFactory? is available on the settings initializer.
The factory is a synchronous throwing @Sendable closure; captured stores/clocks must be Sendable.
The application calls await settings.tokenManager?.close() when all clients sharing it are done.
Transport closure does not own the shared actor or erase the store. Allow any pending rotation commit
to settle before removing storage.
The hook is invoked once with the resolved provider map, after security validation, and is skipped when no providers are selected. It must only construct a manager: do not acquire tokens or read storage in the hook. Omitting it keeps the existing in-memory default. Settings retain the returned manager, not the factory. All operations on those settings share it; generated aggregate children therefore retain the same cache and single-flight renewal. Independently created managers do not coordinate concurrent refreshes, even if their stores are the same. Reuse a client/aggregate within an active session; use successive managers to reopen saved sessions.
The application owns persistence, encryption, store access and session boundaries. Provider/client,
grant, profile and endpoint identities must distinguish environments and users; the API base URL
alone is not an implicit store namespace. Use a new grant identity for a fresh authorization session.
For logout, stop requests and wait for pending refresh/persistence to finish before removing the
session's store entries, then construct fresh settings. invalidate expires an access token for
renewal; it is not logout and deliberately retains refresh state. No disk storage is enabled automatically.