Skip to content

About

Sunday πŸ™ The framework of REST for Swift

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

Sunday πŸ™ The framework of REST for Swift

GitHub Workflow Status Coverage

Swift framework for generated REST clients.


Swift Package Manager

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>),

Environment-aware credentials

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.

Model validation

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.

License

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.

Typed request parameters

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.

Partial updates

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 variables

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.

Application-owned token persistence

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.

About

Sunday πŸ™ The framework of REST for Swift

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages