diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json new file mode 100644 index 0000000..7bb2864 --- /dev/null +++ b/.github/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)?:(?:\\x1b\\[\\d+m)?(\\d+)(?:\\x1b\\[\\d+m)?:(?:\\x1b\\[\\d+m)?(\\d+)(?:\\x1b\\[\\d+m)?: (?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)? \\[(.+?)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/workflows/lint-swift.yml b/.github/workflows/lint-swift.yml new file mode 100644 index 0000000..aa1a7be --- /dev/null +++ b/.github/workflows/lint-swift.yml @@ -0,0 +1,26 @@ +name: Lint Swift + +on: + pull_request: + branches: + - '*' + paths: + - '**/*.swift' + - 'Package.swift' + - 'Package.resolved' + - '.swiftlint.yml' + - '.github/workflows/lint-swift.yml' + +jobs: + swiftlint: + name: SwiftLint + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install SwiftLint + run: brew install swiftlint + + - name: Run SwiftLint + run: swiftlint lint --strict diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml new file mode 100644 index 0000000..7adb7fa --- /dev/null +++ b/.github/workflows/lint-workflows.yml @@ -0,0 +1,30 @@ +name: Lint Workflows + +on: + pull_request: + branches: + - '*' + paths: + - '.github/workflows/*.yml' + - '.github/actionlint-matcher.json' + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + + - name: Download actionlint + id: get_actionlint + run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + shell: bash + + - name: Register problem matcher + run: echo "::add-matcher::.github/actionlint-matcher.json" + + - name: Run actionlint + run: ${{ steps.get_actionlint.outputs.executable }} -color + shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..22acaa8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test + +on: + push: + branches: + - main + paths: + - '**/*.swift' + - 'Package.swift' + - 'Package.resolved' + - '.github/workflows/test.yml' + pull_request: + branches: + - '*' + paths: + - '**/*.swift' + - 'Package.swift' + - 'Package.resolved' + - '.github/workflows/test.yml' + +jobs: + macos: + name: macOS + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Swift version + run: swift --version + + - name: Build + run: swift build -v + + - name: Run tests + run: swift test -v diff --git a/Package.swift b/Package.swift index 7e529d2..764011c 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( .target( name: "GraphQLAPIKit", dependencies: [ - .product(name: "Apollo", package: "apollo-ios"), + .product(name: "Apollo", package: "apollo-ios") ] ), .testTarget( diff --git a/Sources/GraphQLAPIKit/Errors/GraphQLAPIAdapterError.swift b/Sources/GraphQLAPIKit/Errors/GraphQLAPIAdapterError.swift index 9fe173c..b2e8bf0 100644 --- a/Sources/GraphQLAPIKit/Errors/GraphQLAPIAdapterError.swift +++ b/Sources/GraphQLAPIKit/Errors/GraphQLAPIAdapterError.swift @@ -18,15 +18,13 @@ public enum GraphQLAPIAdapterError: LocalizedError { /// Errors returned by GraphQL API as part of `errors` field case graphQl([GraphQLError]) - init(error: Error) { if let error = error as? GraphQLAPIAdapterError { self = error } else if let error = error as? ApolloError { self = .graphQl(error.errors.map(GraphQLError.init)) } else if let error = error as? URLSessionClient.URLSessionClientError, - case let URLSessionClient.URLSessionClientError.networkError(_, response, underlyingError) = error - { + case let URLSessionClient.URLSessionClientError.networkError(_, response, underlyingError) = error { if let response = response { self = .network(code: response.statusCode, error: underlyingError) } else { diff --git a/Sources/GraphQLAPIKit/Extensions/GraphQLAPIAdapter+Extensions.swift b/Sources/GraphQLAPIKit/Extensions/GraphQLAPIAdapter+Extensions.swift index be7a168..424432b 100644 --- a/Sources/GraphQLAPIKit/Extensions/GraphQLAPIAdapter+Extensions.swift +++ b/Sources/GraphQLAPIKit/Extensions/GraphQLAPIAdapter+Extensions.swift @@ -2,6 +2,7 @@ import Apollo import ApolloAPI import Foundation +// swiftlint:disable:next no_extension_access_modifier public extension GraphQLAPIAdapterProtocol { func fetch( query: Query, diff --git a/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift b/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift index 0b372c7..b86c1a0 100644 --- a/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift +++ b/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift @@ -38,12 +38,14 @@ public protocol GraphQLAPIAdapterProtocol: AnyObject { public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol { private let apollo: ApolloClientProtocol + // swiftlint:disable function_default_parameter_at_end public init( url: URL, urlSessionConfiguration: URLSessionConfiguration = .default, defaultHeaders: [String: String] = [:], networkObservers: repeat each Observer ) { + // swiftlint:enable function_default_parameter_at_end var observers: [any GraphQLNetworkObserver] = [] repeat observers.append(each networkObservers) @@ -68,7 +70,7 @@ public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol { url: URL, urlSessionConfiguration: URLSessionConfiguration = .default, defaultHeaders: [String: String] = [:], - networkObservers: [any GraphQLNetworkObserver] + networkObservers: [any GraphQLNetworkObserver] = [] ) { let provider = NetworkInterceptorProvider( client: URLSessionClient(sessionConfiguration: urlSessionConfiguration), diff --git a/Sources/GraphQLAPIKit/Interceptors/NetworkInterceptorProvider.swift b/Sources/GraphQLAPIKit/Interceptors/NetworkInterceptorProvider.swift index 4b55acf..06c9566 100644 --- a/Sources/GraphQLAPIKit/Interceptors/NetworkInterceptorProvider.swift +++ b/Sources/GraphQLAPIKit/Interceptors/NetworkInterceptorProvider.swift @@ -21,7 +21,7 @@ struct NetworkInterceptorProvider: InterceptorProvider { func interceptors(for operation: Operation) -> [ApolloInterceptor] { // Headers first, then before-observers, then network fetch, then after-observers [ - RequestHeaderInterceptor(defaultHeaders: defaultHeaders), + RequestHeaderInterceptor(defaultHeaders: defaultHeaders) ] + pairOfObserverInterceptors.map(\.before) // Before network - captures timing + [ @@ -35,8 +35,8 @@ struct NetworkInterceptorProvider: InterceptorProvider { JSONResponseParsingInterceptor() ] } - - static private func makePair(of observer: T) -> (before: ApolloInterceptor, after: ApolloInterceptor) { + + private static func makePair(of observer: T) -> (before: ApolloInterceptor, after: ApolloInterceptor) { let contextStore = ObserverContextStore() let beforeInterceptor = ObserverInterceptor(observer: observer, contextStore: contextStore) let afterInterceptor = ObserverInterceptor(observer: observer, contextStore: contextStore) diff --git a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterErrorTests.swift b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterErrorTests.swift index 54e8e98..76b411f 100644 --- a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterErrorTests.swift +++ b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterErrorTests.swift @@ -1,6 +1,6 @@ -import XCTest import Apollo @testable import GraphQLAPIKit +import XCTest final class GraphQLAPIAdapterErrorTests: XCTestCase { func testGraphQLAPIAdapterErrorPassthrough() { @@ -46,12 +46,14 @@ final class GraphQLAPIAdapterErrorTests: XCTestCase { code: 500, userInfo: [NSLocalizedDescriptionKey: "Server error"] ) + // swiftlint:disable force_unwrapping let response = HTTPURLResponse( url: URL(string: "https://example.com")!, statusCode: 500, httpVersion: nil, headerFields: nil )! + // swiftlint:enable force_unwrapping let urlSessionError = URLSessionClient.URLSessionClientError.networkError( data: Data(), response: response, diff --git a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterIntegrationTests.swift b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterIntegrationTests.swift index 5635d4a..86dfa8d 100644 --- a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterIntegrationTests.swift +++ b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterIntegrationTests.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI -import XCTest @testable import GraphQLAPIKit +import XCTest // MARK: - MockURLProtocol @@ -22,11 +22,11 @@ final class MockURLProtocol: URLProtocol { mockError = nil } - override class func canInit(with request: URLRequest) -> Bool { + override static func canInit(with request: URLRequest) -> Bool { true } - override class func canonicalRequest(for request: URLRequest) -> URLRequest { + override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } @@ -44,12 +44,16 @@ final class MockURLProtocol: URLProtocol { statusCode: 200 ) + guard let url = request.url else { + return + } + let httpResponse = HTTPURLResponse( - url: request.url!, + url: url, statusCode: response.statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"] - )! + )! // swiftlint:disable:this force_unwrapping client?.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: response.data) @@ -60,9 +64,9 @@ final class MockURLProtocol: URLProtocol { /// A valid GraphQL response with minimal data private var validGraphQLResponse: Data { - """ + Data(""" {"data": {"__typename": "Query"}} - """.data(using: .utf8)! + """.utf8) } } @@ -72,7 +76,9 @@ enum MockSchema: SchemaMetadata { static let configuration: any SchemaConfiguration.Type = MockSchemaConfiguration.self static func objectType(forTypename typename: String) -> Object? { - if typename == "Query" { return MockQuery.Data.self.__parentType as? Object } + if typename == "Query" { + return MockQuery.Data.self.__parentType as? Object + } return nil } } @@ -94,6 +100,7 @@ final class MockQuery: GraphQLQuery { init() {} + // swiftlint:disable nesting identifier_name struct MockQueryData: RootSelectionSet { typealias Schema = MockSchema @@ -106,6 +113,7 @@ final class MockQuery: GraphQLQuery { self.__data = _dataDict } } + // swiftlint:enable nesting identifier_name } // MARK: - Mock Request Headers @@ -172,6 +180,7 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { ] let adapter = GraphQLAPIAdapter( + // swiftlint:disable:next force_unwrapping url: URL(string: "https://api.example.com/graphql")!, urlSessionConfiguration: mockSessionConfiguration(), defaultHeaders: defaultHeaders, @@ -207,10 +216,10 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { ]) let adapter = GraphQLAPIAdapter( + // swiftlint:disable:next force_unwrapping url: URL(string: "https://api.example.com/graphql")!, urlSessionConfiguration: mockSessionConfiguration(), - defaultHeaders: [:], - networkObservers: observer + networkObservers: [observer] ) _ = adapter.fetch(query: MockQuery(), context: contextHeaders, queue: .main) { _ in @@ -246,6 +255,7 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { ]) let adapter = GraphQLAPIAdapter( + // swiftlint:disable:next force_unwrapping url: URL(string: "https://api.example.com/graphql")!, urlSessionConfiguration: mockSessionConfiguration(), defaultHeaders: defaultHeaders, @@ -282,10 +292,13 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { let defaultHeaders = ["X-Shared-Header": "shared-value"] let adapter = GraphQLAPIAdapter( + // swiftlint:disable:next force_unwrapping url: URL(string: "https://api.example.com/graphql")!, urlSessionConfiguration: mockSessionConfiguration(), defaultHeaders: defaultHeaders, - networkObservers: observer1, observer2, observer3 + networkObservers: observer1, + observer2, + observer3 ) _ = adapter.fetch(query: MockQuery(), context: nil, queue: .main) { _ in @@ -319,10 +332,10 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { let observer = IntegrationMockObserver() let adapter = GraphQLAPIAdapter( + // swiftlint:disable:next force_unwrapping url: URL(string: "https://api.example.com/graphql")!, urlSessionConfiguration: mockSessionConfiguration(), - defaultHeaders: [:], - networkObservers: observer + networkObservers: [observer] ) _ = adapter.fetch(query: MockQuery(), context: nil, queue: .main) { _ in @@ -340,5 +353,4 @@ final class GraphQLAPIAdapterIntegrationTests: XCTestCase { XCTAssertEqual(capturedRequest.value(forHTTPHeaderField: "X-APOLLO-OPERATION-NAME"), "MockQuery") XCTAssertNotNil(capturedRequest.value(forHTTPHeaderField: "Content-Type")) } - } diff --git a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterTests.swift b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterTests.swift index d34bd62..43751e0 100644 --- a/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterTests.swift +++ b/Tests/GraphQLAPIKitTests/GraphQLAPIAdapterTests.swift @@ -1,9 +1,10 @@ import Apollo import ApolloAPI -import XCTest @testable import GraphQLAPIKit +import XCTest final class GraphQLAPIAdapterTests: XCTestCase { + // swiftlint:disable:next force_unwrapping let testURL = URL(string: "https://api.example.com/graphql")! // MARK: - Initialization Tests @@ -37,5 +38,4 @@ final class GraphQLAPIAdapterTests: XCTestCase { ) XCTAssertNotNil(adapter) } - } diff --git a/Tests/GraphQLAPIKitTests/GraphQLErrorTests.swift b/Tests/GraphQLAPIKitTests/GraphQLErrorTests.swift index f81ad2f..324ad0c 100644 --- a/Tests/GraphQLAPIKitTests/GraphQLErrorTests.swift +++ b/Tests/GraphQLAPIKitTests/GraphQLErrorTests.swift @@ -1,6 +1,6 @@ -import XCTest import Apollo @testable import GraphQLAPIKit +import XCTest final class GraphQLErrorTests: XCTestCase { func testGraphQLErrorWithMessageAndCode() { diff --git a/Tests/GraphQLAPIKitTests/GraphQLNetworkObserverTests.swift b/Tests/GraphQLAPIKitTests/GraphQLNetworkObserverTests.swift index 70fbba6..9f62320 100644 --- a/Tests/GraphQLAPIKitTests/GraphQLNetworkObserverTests.swift +++ b/Tests/GraphQLAPIKitTests/GraphQLNetworkObserverTests.swift @@ -1,12 +1,14 @@ -import XCTest @testable import GraphQLAPIKit +import XCTest final class GraphQLNetworkObserverTests: XCTestCase { // MARK: - MockObserver + // swiftlint:disable nesting final class MockObserver: GraphQLNetworkObserver { struct Context: Sendable { + // swiftlint:enable nesting let requestId: String let startTime: Date } @@ -60,6 +62,7 @@ final class GraphQLNetworkObserverTests: XCTestCase { func testProtocolMethodSignatures() { let observer = MockObserver() + // swiftlint:disable:next force_unwrapping let url = URL(string: "https://api.example.com/graphql")! var request = URLRequest(url: url)