diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 680623b..672e272 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -5,6 +5,7 @@ on: tags: - 'v*' - '[0-9]*' + workflow_dispatch: jobs: diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ebfae0b..fe7fad0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -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 @@ -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 @@ -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\"}]" \ No newline at end of file + run_tests_swift_versions: '["6.1","6.2"]' \ No newline at end of file diff --git a/Makefile b/Makefile index 11bd428..53b8568 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/Package.swift b/Package.swift index 806eec0..160107d 100644 --- a/Package.swift +++ b/Package.swift @@ -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) @@ -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 + ), ] ) diff --git a/README.md b/README.md index 84b7cf0..6b39363 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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. @@ -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` diff --git a/Sources/FeatherSpec/Models/Expectation.swift b/Sources/FeatherSpec/Models/Expectation.swift index c8ab764..edac10c 100644 --- a/Sources/FeatherSpec/Models/Expectation.swift +++ b/Sources/FeatherSpec/Models/Expectation.swift @@ -10,12 +10,12 @@ 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. /// @@ -23,7 +23,7 @@ public struct Expectation { /// /// - 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 } @@ -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 diff --git a/Sources/FeatherSpec/Models/Spec.swift b/Sources/FeatherSpec/Models/Spec.swift index b8e3acf..68babc9 100644 --- a/Sources/FeatherSpec/Models/Spec.swift +++ b/Sources/FeatherSpec/Models/Spec.swift @@ -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. /// @@ -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)) } @@ -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) } } @@ -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) @@ -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) } } diff --git a/Sources/FeatherSpec/Protocols/SpecExecutor.swift b/Sources/FeatherSpec/Protocols/SpecExecutor.swift index 8857907..748cad3 100644 --- a/Sources/FeatherSpec/Protocols/SpecExecutor.swift +++ b/Sources/FeatherSpec/Protocols/SpecExecutor.swift @@ -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. /// diff --git a/Sources/FeatherSpec/Protocols/SpecRunner.swift b/Sources/FeatherSpec/Protocols/SpecRunner.swift index b1c27b8..8bad4fb 100644 --- a/Sources/FeatherSpec/Protocols/SpecRunner.swift +++ b/Sources/FeatherSpec/Protocols/SpecRunner.swift @@ -7,7 +7,7 @@ /// SpecRunner. /// /// Conforming types provide the execution environment for one or more specs. -public protocol SpecRunner { +public protocol SpecRunner: Sendable { /// Asynchronously tests a specification. /// @@ -15,9 +15,8 @@ public protocol SpecRunner { /// /// - 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? diff --git a/Sources/FeatherSpec/SpecBuilderParameters/Expectations/ExpectBuilderParam.swift b/Sources/FeatherSpec/SpecBuilderParameters/Expectations/ExpectBuilderParam.swift index 9b118cc..f2e818e 100644 --- a/Sources/FeatherSpec/SpecBuilderParameters/Expectations/ExpectBuilderParam.swift +++ b/Sources/FeatherSpec/SpecBuilderParameters/Expectations/ExpectBuilderParam.swift @@ -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) } @@ -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) } diff --git a/Tests/FeatherSpecTests/FeatherSpecTests.swift b/Tests/FeatherSpecTests/FeatherSpecTestSuite.swift similarity index 96% rename from Tests/FeatherSpecTests/FeatherSpecTests.swift rename to Tests/FeatherSpecTests/FeatherSpecTestSuite.swift index 09824d4..16bd73e 100644 --- a/Tests/FeatherSpecTests/FeatherSpecTests.swift +++ b/Tests/FeatherSpecTests/FeatherSpecTestSuite.swift @@ -1,5 +1,5 @@ // -// FeatherSpecTests.swift +// FeatherSpecTestSuite.swift // feather-spec // // Created by Binary Birds on 2026. 01. 20.. @@ -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. /// @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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() diff --git a/Tests/FeatherSpecTests/MockRunner/Custom.swift b/Tests/FeatherSpecTests/MockRunner/Custom.swift index 1d079c9..1680bde 100644 --- a/Tests/FeatherSpecTests/MockRunner/Custom.swift +++ b/Tests/FeatherSpecTests/MockRunner/Custom.swift @@ -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) }