Skip to content

Upgrade everything to Swift 5.9 and Sendable correctness, use SwiftHTTPTypes #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
92 changes: 62 additions & 30 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
name: test
name: Tests
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
pull_request:
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }
env:
LOG_LEVEL: info

jobs:
appleos:
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
xcode:
- latest
#- latest-stable
destination:
- 'platform=macOS,arch=x86_64'
#- 'platform=macOS,arch=arm64'
- 'platform=iOS Simulator,OS=latest,name=iPhone 15 Pro'
- 'platform=tvOS Simulator,OS=latest,name=Apple TV 4K'
- 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 9 - 45mm'
platform:
- 'macOS'
- 'iOS Simulator'
- 'tvOS Simulator'
- 'watchOS Simulator'
include:
- platform: 'macOS'
destination: 'arch=x86_64'
- platform: 'iOS Simulator'
destination: 'OS=latest,name=iPhone 15 Pro'
- platform: 'tvOS Simulator'
destination: 'OS=latest,name=Apple TV 4K (3rd generation)'
- platform: 'watchOS Simulator'
destination: 'OS=latest,name=Apple Watch Series 9 (45mm)'
name: ${{ matrix.platform }} Tests
runs-on: macos-13
steps:
- name: Select latest available Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Checkout
uses: actions/checkout@v4
- name: Run tests for ${{ matrix.destination }}
run: |
xcodebuild test -scheme StructuredAPIClient-Package \
-enableThreadSanitizer YES \
-destination '${{ matrix.destination }}'
- name: Select latest available Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Install xcbeautify
run: brew install xcbeautify
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
env:
DESTINATION: ${{ format('platform={0},{1}', matrix.platform, matrix.destination) }}
run: |
set -o pipefail && \
xcodebuild test -scheme StructuredAPIClient-Package \
-enableThreadSanitizer YES \
-enableCodeCoverage YES \
-disablePackageRepositoryCache \
-destination "${DESTINATION}" |
xcbeautify --is-ci --quiet --renderer github-actions

linux:
runs-on: ubuntu-latest
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
ver:
swift-image:
- swift:5.9-jammy
- swiftlang/swift:nightly-5.10-jammy
- swiftlang/swift:nightly-main-jammy
container:
image: ${{ matrix.ver }}
name: Linux ${{ matrix.swift-image }} Tests
runs-on: ubuntu-latest
container: ${{ matrix.swift-image }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run tests for ${{ matrix.ver }}
run: swift test --sanitize=thread
- name: Checkout code
uses: actions/checkout@v4
- name: Install xcbeautify
run: |
DEBIAN_FRONTEND=noninteractive apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y xz-utils curl
curl -fsSLO 'https://github.com/tuist/xcbeautify/releases/download/1.0.1/xcbeautify-1.0.1-x86_64-unknown-linux-gnu.tar.xz'
tar -x -J -f xcbeautify-1.0.1-x86_64-unknown-linux-gnu.tar.xz
- name: Run tests
shell: bash
run: |
set -o pipefail && \
swift test --sanitize=thread --enable-code-coverage |
./xcbeautify --is-ci --quiet --renderer github-actions
15 changes: 12 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

import PackageDescription

let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableExperimentalFeature("StrictConcurrency=complete"),
]

let package = Package(
name: "StructuredAPIClient",
products: [
Expand All @@ -32,21 +40,22 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
.product(name: "HTTPTypes", package: "swift-http-types"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
swiftSettings: swiftSettings
),
.target(
name: "StructuredAPIClientTestSupport",
dependencies: [
.target(name: "StructuredAPIClient"),
],
swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")]
swiftSettings: swiftSettings
),
.testTarget(
name: "StructuredAPIClientTests",
dependencies: [
.target(name: "StructuredAPIClient"),
.target(name: "StructuredAPIClientTestSupport"),
]
],
swiftSettings: swiftSettings
),
]
)
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
# StructuredAPIClient
# <div align="center">StructuredAPIClient</div>

<p align="center">

<a href="LICENSE">
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
<img src="https://img.shields.io/badge/license-MIT-skyblue?style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSJza3libHVlIiBkPSJNNzAuNTQsMTEuNWMtLjEtMy44Ny0xMi0zLjg3LTEyLDB2MTBjLTUuMjcuMi0yMC4zNCw3Ljg3LTI0LjQ0LDhoLTE4LjRjLTcuMSwwLTguMywxMy45NSwzLjUsMTIsMCwwLTE0Ljk2LDMzLjItMTYuNywzNy41NC04LjE1LDE4LjQ2LDUzLjkzLDE3LjMsNDUuOC44LTIuNS01LjA3LTE2LjctMzguMzQtMTYuNy0zOC4zNCw1LjcsMCwxOC40LTcuODMsMjcuMTQtOHY3Ni43OGgtMjBjLTMuOSwwLTMuOSwxMiwwLDEyaDUyYzMuOSwwLDMuOS0xMiwwLTEyaC0yMHYtNzYuNzhjOC43LS4xLDIxLjE2LDgsMjcuMzYsOCwwLDAtMTQuNDMsMzIuODYtMTYuNywzOC4zNC03LjEsMTUuOTYsNTMuNTYsMTguMyw0NS44LDAtMi4yLTUuMy0xNi43LTM4LjM0LTE2LjctMzguMzQsMTIuNiwxLjIsMTEuNS0xMiwzLjUtMTIsMCwwLTIyLjgtLjItMTguNCwwLDAsMC0xOS03LjktMjQuNDQtOHptMzIuODYsNDQuNjYsMTAuNCwyNGMtMy45LDEuNzQtMTguNiwxLjItMjAuODQsMHptLTc3LjcsMCwxMC40LDI0Yy04LDMuMy0xNSwyLjktMjAuODQsMCwzLjcyLTcuODcsNi41Ny0xNi4yNSwxMC40NC0yNHoiLz48L3N2Zz4%3D" alt="MIT License">
</a>
<a href="https://swift.org">
<img src="https://img.shields.io/badge/swift-5.3-brightgreen.svg" alt="Swift 5.3">

<a href="https://github.com/stairtree/StructuredAPIClient/actions/workflows/test.yml">
<img src="https://img.shields.io/github/actions/workflow/status/stairtree/StructuredAPIClient/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="CI">
</a>
<a href="https://github.com/stairtree/StructuredAPIClient/actions">
<img src="https://github.com/stairtree/StructuredAPIClient/workflows/test/badge.svg" alt="CI">

<a href="https://swift.org">
<img src="https://img.shields.io/badge/swift-5.9%2b-white?style=plastic&logoColor=%23f07158&labelColor=gray&color=%23f07158&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZD0iTTYsMjRjLTMsMC02LTMtNi02di0xMmMwLTMsMy02LDYtNmgxMmMzLDAsNiwzLDYsNnYxMmMwLDMtMyw2LTYsNnoiIGZpbGw9IiNmMDcxNTgiLz48cGF0aCBkPSJNMTMuNiwzLjRjNC4yLDIuNCw2LjMsNy41LDUuMywxMS41LDEuOSwyLjgsMS42LDUuMiwxLjQsNC43LTEuMi0yLjMtMy4zLTEuNC00LjQtLjctMy45LDEuOC0xMC4yLjItMTMuNS01LDMsMi4yLDcuMiwzLjEsMTAuMywxLjItNC42LTMuNi04LjUtOS4yLTguNS05LjMsMi4zLDIuMSw2LDQuOCw3LjMsNS43LTIuOC0zLjEtNS4zLTYuNy01LjItNi43LDIuNywyLjcsNS43LDUuMiw4LjksNy4yLjQtLjgsMS40LTQuNS0xLjYtOC43eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg%3D%3D" alt="Swift 5.9">
</a>

</p>

A testable and composable network client.

### Supported Platforms
For more information, browse the API documentation (_coming soon_).

## Supported Platforms

StructuredAPIClient is tested on macOS, iOS, tvOS, Linux, and Windows, and is known to support the following operating system versions:

* Ubuntu 16.04+
* Ubuntu 18.04+
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 5.9 even support 18.04? It's EOL so you should probably raise it to 20.04

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, you're right, 5.9 only goes back to focal.

* AmazonLinux2
* macOS 10.12+
* iOS 12+
Expand All @@ -28,10 +36,10 @@ To integrate the package:

```swift
dependencies: [
.package(url: "https://github.com/stairtree/StructuredAPIClient.git", from: "1.1.1")
.package(url: "https://github.com/stairtree/StructuredAPIClient.git", from: "2.0.0")
]
```

---

Inspired by blog posts by [Rob Napier](https://robnapier.net) and [Soroush Khanlou](http://khanlou.com), as well as the [Testing Tips & Tricks](https://developer.apple.com/videos/play/wwdc2018/417/) WWDC talk.
Inspired by blog posts by [Rob Napier](https://robnapier.net) and [Soroush Khanlou](http://khanlou.com), as well as the [Testing Tips & Tricks](https://developer.apple.com/videos/play/wwdc2018/417/) WWDC talk. Version 2.0 revised for full Concurrency support by @gwynne.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public final class AddHTTPHeadersHandler: Transport {
/// The base `Transport` to extend with extra headers.
///
/// - Note: Never `nil` in practice for this transport.
public let next: Transport?
public let next: (any Transport)?

/// Additional headers that will be applied to the request upon sending.
private let headers: [String: String]
Expand All @@ -55,13 +55,13 @@ public final class AddHTTPHeadersHandler: Transport {
/// - base: The base `Transport` that will have the headers applied
/// - headers: Headers to apply to the base `Transport`
/// - mode: The mode to use for resolving conflicts between a request's headers and the transport's headers.
public init(base: Transport, headers: [String: String], mode: Mode = .add) {
public init(base: any Transport, headers: [String: String], mode: Mode = .add) {
self.next = base
self.headers = headers
self.mode = mode
}

public func send(request: URLRequest, completion: @escaping @Sendable (Result<TransportResponse, Error>) -> Void) {
public func send(request: URLRequest, completion: @escaping @Sendable (Result<TransportResponse, any Error>) -> Void) {
var newRequest = request

for (key, value) in self.headers {
Expand Down
72 changes: 0 additions & 72 deletions Sources/StructuredAPIClient/Handlers/BackgroundTaskHandler.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import Logging

// Handle token auth and add appropriate auth headers to an existing transport.
public final class TokenAuthenticationHandler: Transport {
public let next: Transport?
public let next: (any Transport)?
private let logger: Logger
private let auth: AuthState

public init(base: Transport, accessToken: Token? = nil, refreshToken: Token? = nil, tokenProvider: TokenProvider, logger: Logger? = nil) {
public init(base: any Transport, accessToken: (any Token)? = nil, refreshToken: (any Token)? = nil, tokenProvider: any TokenProvider, logger: Logger? = nil) {
self.next = base
self.logger = logger ?? Logger(label: "TokenAuth")
self.auth = AuthState(accessToken: accessToken, refreshToken: refreshToken, provider: tokenProvider, logger: logger)
}

public func send(request: URLRequest, completion: @escaping @Sendable (Result<TransportResponse, Error>) -> Void) {
public func send(request: URLRequest, completion: @escaping @Sendable (Result<TransportResponse, any Error>) -> Void) {
self.auth.token { result in
switch result {
case let .failure(error):
Expand All @@ -46,10 +46,10 @@ public final class TokenAuthenticationHandler: Transport {

public protocol TokenProvider {
// Get access token and refresh token
func fetchToken(completion: @escaping @Sendable (Result<(Token, Token), Error>) -> Void)
func fetchToken(completion: @escaping @Sendable (Result<(any Token, any Token), any Error>) -> Void)

// Refreh the current token
func refreshToken(withRefreshToken refreshToken: Token, completion: @escaping @Sendable (Result<Token, Error>) -> Void)
func refreshToken(withRefreshToken refreshToken: any Token, completion: @escaping @Sendable (Result<any Token, any Error>) -> Void)
}

public protocol Token: Sendable {
Expand All @@ -60,32 +60,32 @@ public protocol Token: Sendable {
final class AuthState: @unchecked Sendable {
private final class LockedTokens: @unchecked Sendable {
private let lock = NSLock()
private var accessToken: Token?
private var refreshToken: Token?
private var accessToken: (any Token)?
private var refreshToken: (any Token)?

init(accessToken: Token?, refreshToken: Token?) {
init(accessToken: (any Token)?, refreshToken: (any Token)?) {
self.accessToken = accessToken
self.refreshToken = refreshToken
}

func withLock<R>(_ closure: @escaping @Sendable (inout Token?, inout Token?) throws -> R) rethrows -> R {
func withLock<R>(_ closure: @escaping @Sendable (inout (any Token)?, inout (any Token)?) throws -> R) rethrows -> R {
try self.lock.withLock {
try closure(&self.accessToken, &self.refreshToken)
}
}
}

private let tokens: LockedTokens
let provider: TokenProvider
let provider: any TokenProvider
let logger: Logger

internal init(accessToken: Token? = nil, refreshToken: Token? = nil, provider: TokenProvider, logger: Logger? = nil) {
internal init(accessToken: (any Token)? = nil, refreshToken: (any Token)? = nil, provider: any TokenProvider, logger: Logger? = nil) {
self.tokens = .init(accessToken: accessToken, refreshToken: refreshToken)
self.provider = provider
self.logger = logger ?? Logger(label: "AuthState")
}

func token(_ completion: @escaping @Sendable (Result<String, Error>) -> Void) {
func token(_ completion: @escaping @Sendable (Result<String, any Error>) -> Void) {
if let raw = self.tokens.withLock({ token, _ in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0.raw : nil } }) {
return completion(.success(raw))
} else if let refresh = self.tokens.withLock({ _, token in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0 : nil } }) {
Expand Down
Loading