Skip to content
This repository was archived by the owner on May 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
tags:
- 'v*'
- '[0-9]*'
workflow_dispatch:

jobs:

Expand Down
18 changes: 8 additions & 10 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:

jobs:

api_breakage:
name: Check API breakage
uses: BinaryBirds/github-workflows/.github/workflows/api_breakage.yml@main

swiftlang_checks:
name: Swiftlang Checks
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
Expand All @@ -15,7 +19,7 @@ jobs:
format_check_enabled : true
broken_symlink_check_enabled : true
unacceptable_language_check_enabled : true
shell_check_enabled : true
shell_check_enabled : false
docs_check_enabled : false
api_breakage_check_enabled : false
license_header_check_enabled : false
Expand All @@ -27,13 +31,7 @@ jobs:
uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main
with:
local_swift_dependencies_check_enabled : true
headers_check_enabled : true
run_tests_with_cache_enabled : true
headers_check_enabled : false
docc_warnings_check_enabled : true

swiftlang_tests:
name: Swiftlang Tests
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
enable_windows_checks : false
linux_build_command: "swift test --parallel --enable-code-coverage"
linux_exclude_swift_versions: "[{\"swift_version\": \"5.8\"}, {\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"nightly-6.0\"}, {\"swift_version\": \"nightly-6.1\"}, {\"swift_version\": \"nightly-6.3\"}]"
run_tests_swift_versions: '["6.1","6.2"]'
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ SHELL=/bin/bash

baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts

check: symlinks language deps lint docc-warnings headers
check: symlinks language deps lint docc-warnings headers package

package:
curl -s $(baseUrl)/check-swift-package.sh | bash

breakage:
curl -s $(baseUrl)/check-api-breakage.sh | bash

symlinks:
curl -s $(baseUrl)/check-broken-symlinks.sh | bash
Expand Down
22 changes: 15 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var defaultSwiftSettings: [SwiftSetting] =
// https://forums.swift.org/t/experimental-support-for-lifetime-dependencies-in-swift-6-2-and-beyond/78638
.enableExperimentalFeature("Lifetimes"),
// https://github.com/swiftlang/swift/pull/65218
.enableExperimentalFeature("AvailabilityMacro=featherDatabase 1.0:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"),
.enableExperimentalFeature("AvailabilityMacro=featherSpec 1.0:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"),
]

#if compiler(>=6.2)
Expand All @@ -34,14 +34,22 @@ let package = Package(
.library(name: "FeatherSpec", targets: ["FeatherSpec"]),
],
dependencies: [
// [docc-plugin-placeholder]
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
],
targets: [
.target(name: "FeatherSpec", dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
]),
.testTarget(name: "FeatherSpecTests", dependencies: [
.target(name: "FeatherSpec"),
]),
.target(name: "FeatherSpec",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"
)
],
swiftSettings: defaultSwiftSettings
),
.testTarget(name: "FeatherSpecTests",
dependencies: [
.target(name: "FeatherSpec"),
],
swiftSettings: defaultSwiftSettings
),
]
)
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Declarative HTTP specification testing for Swift, providing a shared API surface for runners and executors.

![Release: 1.0.0-beta.1](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E1-F05138)
[![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1.0.0--beta.2-F05138)]( https://github.com/feather-framework/feather-spec/releases/tag/1.0.0-beta.2)

## Features

Expand All @@ -14,22 +14,23 @@ Declarative HTTP specification testing for Swift, providing a shared API surface
## Requirements

![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138)
![Platforms: macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138)
![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138)

- Swift 6.1+
- Platforms:
- macOS 15+
- iOS 18+
- tvOS 18+
- watchOS 11+
- visionOS 2+
- Linux
- macOS 15+
- iOS 18+
- tvOS 18+
- watchOS 11+
- visionOS 2+

## Installation

Use Swift Package Manager; add the dependency to your `Package.swift` file:

```swift
.package(url: "https://github.com/feather-framework/feather-spec", exact: "1.0.0-beta.1"),
.package(url: "https://github.com/feather-framework/feather-spec", exact: "1.0.0-beta.2"),
```

Then add `FeatherSpec` to your target dependencies:
Expand All @@ -40,9 +41,9 @@ Then add `FeatherSpec` to your target dependencies:

## Usage

![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)
[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-spec/)

API documentation is available at the following [link](https://feather-framework.github.io/feather-spec/). Refer to the mock objects in the Tests directory if you want to build a custom runner or executor implementation.
API documentation is available at the following link. Refer to the mock objects in the Tests directory if you want to build a custom runner or executor implementation.

> [!WARNING]
> This repository is a work in progress, things can break until it reaches v1.0.0.
Expand All @@ -58,8 +59,8 @@ The following Swift server-side runtime integrations are available:

- Build: `swift build`
- Test:
- local: `make test`
- using Docker: `make docker-test`
- local: `make test`
- using Docker: `make docker-test`
- Format: `make format`
- Check: `make check`

Expand Down
8 changes: 4 additions & 4 deletions Sources/FeatherSpec/Models/Expectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import OpenAPIRuntime
/// A structure representing an expectation to be used in building specifications.
///
/// Expectations encapsulate async validation logic for responses.
public struct Expectation {
public struct Expectation: Sendable {

/// A closure representing the expectation block.
///
/// The block receives the response and body produced by the executor.
public let block: ((HTTPResponse, HTTPBody) async throws -> Void)
public let block: (@Sendable (HTTPResponse, HTTPBody) async throws -> Void)

/// Initializes an `Expectation` instance with the specified parameters.
///
/// The provided block is stored for deferred execution.
///
/// - Parameter block: The closure representing the expectation block.
public init(
block: @escaping ((HTTPResponse, HTTPBody) async throws -> Void)
block: @escaping @Sendable (HTTPResponse, HTTPBody) async throws -> Void
) {
self.block = block
}
Expand Down Expand Up @@ -62,7 +62,7 @@ extension Expectation {
/// - Returns: An `Expectation` instance for verifying the presence of a specific HTTP header.
public static func header(
name: HTTPField.Name,
block: ((String) async throws -> Void)? = nil
block: (@Sendable (String) async throws -> Void)? = nil
) -> Self {
.init(
block: { response, _ in
Expand Down
12 changes: 7 additions & 5 deletions Sources/FeatherSpec/Models/Spec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import OpenAPIRuntime
/// A structure representing an HTTP request specification.
///
/// This holds request data and expectations to be evaluated after execution.
public struct Spec {
public struct Spec: Sendable {

/// The HTTP request.
///
Expand Down Expand Up @@ -75,7 +75,8 @@ public struct Spec {
///
/// The expectation runs after the request completes.
public mutating func addExpectation(
_ block: @escaping ((HTTPResponse, HTTPBody) async throws -> Void)
_ block:
@escaping @Sendable (HTTPResponse, HTTPBody) async throws -> Void
) {
expectations.append(.init(block: block))
}
Expand Down Expand Up @@ -180,7 +181,8 @@ public struct Spec {
///
/// Returns a new spec with the expectation appended.
public func expect(
_ block: @escaping ((HTTPResponse, HTTPBody) async throws -> Void)
_ block:
@escaping @Sendable (HTTPResponse, HTTPBody) async throws -> Void
) -> Self {
modify { $0.addExpectation(block) }
}
Expand Down Expand Up @@ -222,7 +224,7 @@ extension Spec {
/// An optional block can further validate the header value.
public mutating func addExpectation(
_ name: HTTPField.Name,
_ block: ((String) async throws -> Void)? = nil
_ block: (@Sendable (String) async throws -> Void)? = nil
) {
expectations.append(
.header(name: name, block: block)
Expand All @@ -245,7 +247,7 @@ extension Spec {
/// Returns a new spec with the expectation appended.
public func expect(
_ name: HTTPField.Name,
_ block: ((String) async throws -> Void)? = nil
_ block: (@Sendable (String) async throws -> Void)? = nil
) -> Self {
modify { $0.addExpectation(name, block) }
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/FeatherSpec/Protocols/SpecExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import OpenAPIRuntime
/// A protocol defining the interface for running HTTP request specifications.
///
/// Executors turn a `Spec` into a concrete HTTP request and return a response.
public protocol SpecExecutor {
public protocol SpecExecutor: Sendable {

/// Executes an HTTP request specification asynchronously.
///
Expand Down
7 changes: 3 additions & 4 deletions Sources/FeatherSpec/Protocols/SpecRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@
/// SpecRunner.
///
/// Conforming types provide the execution environment for one or more specs.
public protocol SpecRunner {
public protocol SpecRunner: Sendable {

/// Asynchronously tests a specification.
///
/// Implementations typically bridge to a concrete `SpecExecutor`.
///
/// - Parameter block: A closure that takes a `SpecExecutor` and performs asynchronous operations.
/// - Throws: error
func test(
block: @escaping (SpecExecutor) async throws -> Void
) async throws
func test(block: @escaping @Sendable (SpecExecutor) async throws -> Void)
async throws
}

// NOTE: result type?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct Expect: SpecBuilderParameter {
///
/// - Parameter block: A closure that takes an `HTTPResponse` and `HTTPBody` and performs an asynchronous operation.
public init(
block: @escaping ((HTTPResponse, HTTPBody) async throws -> Void)
block: @escaping @Sendable (HTTPResponse, HTTPBody) async throws -> Void
) {
self.expectation = .init(block: block)
}
Expand Down Expand Up @@ -60,7 +60,7 @@ extension Expect {
/// - block: An optional closure that takes a `String` and performs an asynchronous operation.
public init(
_ name: HTTPField.Name,
_ block: ((String) async throws -> Void)? = nil
_ block: (@Sendable (String) async throws -> Void)? = nil
) {
self.expectation = .header(name: name, block: block)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// FeatherSpecTests.swift
// FeatherSpecTestSuite.swift
// feather-spec
//
// Created by Binary Birds on 2026. 01. 20..
Expand All @@ -14,7 +14,7 @@ import Testing
///
/// Covers the fluent API, DSL builder, and executor integrations.
@Suite
struct FeatherSpecTests {
struct FeatherSpecTestSuite {

/// Shared request path used across tests.
///
Expand Down Expand Up @@ -62,6 +62,7 @@ struct FeatherSpecTests {
/// Verifies the mutating `Spec` API.
@Test
func testMutatingFuncSpec() async throws {
let expectedTitle = todo.title
var spec = Spec()
spec.setMethod(.post)
spec.setPath(path)
Expand All @@ -73,7 +74,7 @@ struct FeatherSpecTests {
}
spec.addExpectation { response, body in
let todo = try await body.decode(Todo.self, with: response)
#expect(todo.title == self.todo.title)
#expect(todo.title == expectedTitle)
}

#expect(spec.request.method == .post)
Expand All @@ -88,6 +89,7 @@ struct FeatherSpecTests {
/// Verifies the fluent `Spec` API.
@Test
func testBuilderFuncSpec() async throws {
let expectedTitle = todo.title
let spec = Spec()
.method(.post)
.path(path)
Expand All @@ -99,7 +101,7 @@ struct FeatherSpecTests {
}
.expect { response, body in
let todo = try await body.decode(Todo.self, with: response)
#expect(todo.title == self.todo.title)
#expect(todo.title == expectedTitle)
}

#expect(spec.request.method == .post)
Expand All @@ -114,6 +116,7 @@ struct FeatherSpecTests {
/// Verifies the DSL builder API.
@Test
func testDslSpec() async throws {
let expectedTitle = todo.title
let spec = SpecBuilder {
Method(.post)
Path(path)
Expand All @@ -125,11 +128,11 @@ struct FeatherSpecTests {
}
Expect { response, body in
let todo = try await body.decode(Todo.self, with: response)
#expect(todo.title == self.todo.title)
#expect(todo.title == expectedTitle)
}
Custom { response, body in
let todo = try await body.decode(Todo.self, with: response)
#expect(todo.title == self.todo.title)
#expect(todo.title == expectedTitle)
}
}
.build()
Expand Down
2 changes: 1 addition & 1 deletion Tests/FeatherSpecTests/MockRunner/Custom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct Custom: SpecBuilderParameter {
///
/// The block is wrapped into an `Expectation`.
init(
block: @escaping ((HTTPResponse, HTTPBody) async throws -> Void)
block: @escaping @Sendable (HTTPResponse, HTTPBody) async throws -> Void
) {
self.expectation = .init(block: block)
}
Expand Down
Loading