diff --git a/.vscode/launch.json b/.vscode/launch.json index bb04c5be..ac3468df 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,7 @@ "type": "lldb", "request": "launch", "name": "Debug swift-graphql", - "program": ".build/debug/swift-graphql", + "program": "${workspaceFolder:swift-graphql}/.build/debug/swift-graphql", "args": [], "cwd": "${workspaceFolder:swift-graphql}", "preLaunchTask": "swift: Build Debug swift-graphql" @@ -13,7 +13,7 @@ "type": "lldb", "request": "launch", "name": "Release swift-graphql", - "program": ".build/release/swift-graphql", + "program": "${workspaceFolder:swift-graphql}/.build/release/swift-graphql", "args": [], "cwd": "${workspaceFolder:swift-graphql}", "preLaunchTask": "swift: Build Release swift-graphql" @@ -23,9 +23,11 @@ "request": "launch", "name": "Test swift-graphql", "program": "/Applications/Xcode.app/Contents/Developer/usr/bin/xctest", - "args": [".build/debug/swift-graphqlPackageTests.xctest"], + "args": [ + ".build/debug/swift-graphqlPackageTests.xctest" + ], "cwd": "${workspaceFolder:swift-graphql}", "preLaunchTask": "swift: Build All" } ] -} +} \ No newline at end of file diff --git a/Formula/SwiftGraphQL.rb b/Formula/SwiftGraphQL.rb index 3f99548d..03e2294b 100644 --- a/Formula/SwiftGraphQL.rb +++ b/Formula/SwiftGraphQL.rb @@ -2,21 +2,20 @@ class Swiftgraphql < Formula desc "Code generator for SwiftGraphQL library" homepage "https://swift-graphql.org" license "MIT" + version "5.1.2" - url "https://github.com/maticzav/swift-graphql/archive/5.1.2.tar.gz" - sha256 "a3b7af209356dc1362b807fd9a4f33dde47c2b815e034e6c7016f19968e8d33e" + url "file:///Users/bryananderson/Developer/Forks/swift-graphql", :using => :git, :branch => "swift-6-concurrency" - head "https://github.com/maticzav/swift-graphql.git" - depends_on :xcode uses_from_macos "libxml2" uses_from_macos "swift" def install - system "make", "install", "PREFIX=#{prefix}" + system "swift", "build", "-c", "release", "--product", "swift-graphql" + bin.install ".build/release/swift-graphql" end - def test - system "true" + test do + system "#{bin}/swift-graphql", "--version" end -end +end \ No newline at end of file diff --git a/Package.swift b/Package.swift index 667543b4..05c40ead 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.0 import PackageDescription @@ -6,7 +6,7 @@ let package = Package( name: "swift-graphql", platforms: [ .iOS(.v15), - .macOS(.v10_15), + .macOS(.v12), .tvOS(.v13), .watchOS(.v6) ], @@ -25,7 +25,7 @@ let package = Package( dependencies: [ // .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-format", "508.0.0"..<"510.0.0"), + .package(url: "https://github.com/apple/swift-format", from: "600.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/daltoniam/Starscream.git", from: "4.0.5"), .package(url: "https://github.com/dominicegginton/Spinner", from: "2.0.0"), @@ -69,7 +69,7 @@ let package = Package( dependencies: [ "GraphQLAST", .product(name: "SwiftFormat", package: "swift-format"), - .product(name: "SwiftFormatConfiguration", package: "swift-format"), +// .product(name: "SwiftFormatConfiguration", package: "swift-format"), "SwiftGraphQLUtils" ], path: "Sources/SwiftGraphQLCodegen" diff --git a/Sources/GraphQL/AnyCodable/AnyCodable.swift b/Sources/GraphQL/AnyCodable/AnyCodable.swift index 376dc527..973f108e 100644 --- a/Sources/GraphQL/AnyCodable/AnyCodable.swift +++ b/Sources/GraphQL/AnyCodable/AnyCodable.swift @@ -2,21 +2,21 @@ /** A type-erased `Codable` value. - + The `AnyCodable` type forwards encoding and decoding responsibilities to an underlying value, hiding its specific underlying type. - + You can encode or decode mixed-type values in dictionaries and other collections that require `Encodable` or `Decodable` conformance by declaring their contained type to be `AnyCodable`. - + - SeeAlso: `AnyEncodable` - SeeAlso: `AnyDecodable` */ -@frozen public struct AnyCodable: Codable { +@frozen public struct AnyCodable: Codable, @unchecked Sendable { public let value: Any - - public init(_ value: T?) { + + public init(_ value: T?) { self.value = value ?? () } } @@ -91,40 +91,45 @@ extension AnyCodable: CustomDebugStringConvertible { } extension AnyCodable: ExpressibleByNilLiteral, ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral { - + public init(nilLiteral: ()) { - self.init(nil as Any?) + self.init(nil as Void?) } - + public init(booleanLiteral value: Bool) { self.init(value) } - + public init(integerLiteral value: Int) { self.init(value) } - + public init(floatLiteral value: Double) { self.init(value) } - + public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } - + public init(stringLiteral value: String) { self.init(value) } - - public init(arrayLiteral elements: Any...) { + + public init(arrayLiteral elements: any Sendable...) { self.init(elements) } - - public init(dictionaryLiteral elements: (AnyHashable, Any)...) { - self.init(Dictionary(elements, uniquingKeysWith: { (first, _) in first })) + + public init(dictionaryLiteral elements: (T, any Sendable)...) { + self.init(Dictionary(elements, uniquingKeysWith: { (first, _) in first })) } + + } +public typealias HashSendable = Hashable & Sendable + + extension AnyCodable: Hashable { public func hash(into hasher: inout Hasher) { diff --git a/Sources/GraphQL/AnyCodable/AnyDecodable.swift b/Sources/GraphQL/AnyCodable/AnyDecodable.swift index 419c2f68..ef9ea3fa 100644 --- a/Sources/GraphQL/AnyCodable/AnyDecodable.swift +++ b/Sources/GraphQL/AnyCodable/AnyDecodable.swift @@ -33,7 +33,7 @@ import Foundation let decoder = JSONDecoder() let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json) */ -@frozen public struct AnyDecodable: Decodable { +@frozen public struct AnyDecodable: Decodable, @unchecked Sendable { public let value: Any public init(_ value: T?) { diff --git a/Sources/GraphQL/AnyCodable/AnyEncodable.swift b/Sources/GraphQL/AnyCodable/AnyEncodable.swift index 75aee385..79de9723 100644 --- a/Sources/GraphQL/AnyCodable/AnyEncodable.swift +++ b/Sources/GraphQL/AnyCodable/AnyEncodable.swift @@ -31,7 +31,7 @@ import Foundation let encoder = JSONEncoder() let json = try! encoder.encode(dictionary) */ -@frozen public struct AnyEncodable: Encodable { +@frozen public struct AnyEncodable: Encodable, @unchecked Sendable { public let value: Any public init(_ value: T?) { diff --git a/Sources/GraphQL/Error.swift b/Sources/GraphQL/Error.swift index 50c8af0d..4e1ce3a3 100644 --- a/Sources/GraphQL/Error.swift +++ b/Sources/GraphQL/Error.swift @@ -3,7 +3,7 @@ import Foundation /// A GraphQLError describes an Error found during the parse, validate, or execute phases of performing a GraphQL operation. In addition to a message and stack trace, it also includes information about the locations in a GraphQL document and/or execution result that correspond to the Error. /// /// Its implementation follows the specification described at [GraphQLSpec](http://spec.graphql.org/October2021/#sec-Errors.Error-result-format). -public struct GraphQLError: Codable, Equatable { +public struct GraphQLError: Codable, Equatable, Sendable { /// A short, human-readable summary of the problem. public let message: String diff --git a/Sources/GraphQL/Execution.swift b/Sources/GraphQL/Execution.swift index 82824393..90117d02 100644 --- a/Sources/GraphQL/Execution.swift +++ b/Sources/GraphQL/Execution.swift @@ -3,7 +3,7 @@ import Foundation /// The structure holding parameters for a GraphQL request. /// /// ExecutionArgs contains fields in the [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request-parameters) and [GraphQL over WebSocket](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#subscribe) spec. -public struct ExecutionArgs: Codable, Equatable { +public struct ExecutionArgs: Codable, Equatable, Sendable { /// A Document containing GraphQL Operations and Fragments to execute. public var query: String diff --git a/Sources/GraphQLWebSocket/Client.swift b/Sources/GraphQLWebSocket/Client.swift index 424a9659..2e1355d1 100644 --- a/Sources/GraphQLWebSocket/Client.swift +++ b/Sources/GraphQLWebSocket/Client.swift @@ -9,7 +9,7 @@ import Starscream /// /// - NOTE: The client assumes that you'll manually establish the socket connection /// and that it may send requests. -public class GraphQLWebSocket: WebSocketDelegate { +public final class GraphQLWebSocket: @preconcurrency WebSocketDelegate { /// Configuration of the behaviour of the client. private let config: GraphQLWebSocketConfiguration diff --git a/Sources/SwiftGraphQL/Document/Argument.swift b/Sources/SwiftGraphQL/Document/Argument.swift index 0540ad4e..42ae2882 100644 --- a/Sources/SwiftGraphQL/Document/Argument.swift +++ b/Sources/SwiftGraphQL/Document/Argument.swift @@ -7,7 +7,7 @@ import GraphQL We use it internally in the generated code to pass down information about the field and the type of the field it encodes as well as the value itself. */ -public struct Argument: Hashable { +public struct Argument: Hashable, Sendable { let name: String let type: String let hash: String @@ -40,7 +40,7 @@ public struct Argument: Hashable { // MARK: - Initializer /// Returns a new argument with the given value. - public init(name: String, type: String, value: S) { + public init(name: String, type: String, value: S) { // Argument information self.name = name self.type = type diff --git a/Sources/SwiftGraphQL/Document/Field.swift b/Sources/SwiftGraphQL/Document/Field.swift index 9d3bac0f..abd54652 100644 --- a/Sources/SwiftGraphQL/Document/Field.swift +++ b/Sources/SwiftGraphQL/Document/Field.swift @@ -5,7 +5,7 @@ import SwiftGraphQLUtils /// what the selection should be and what the selected types are. /// /// - NOTE: `interface` field in the `fragment` field may be a union or an interface. -public enum GraphQLField { +public enum GraphQLField: Sendable { public typealias SelectionSet = [GraphQLField] /// Composite field describes a selection on an object an union or an interface. diff --git a/Sources/SwiftGraphQL/Document/Scalar.swift b/Sources/SwiftGraphQL/Document/Scalar.swift index 8442c7e1..04639bc5 100644 --- a/Sources/SwiftGraphQL/Document/Scalar.swift +++ b/Sources/SwiftGraphQL/Document/Scalar.swift @@ -2,7 +2,7 @@ import Foundation import GraphQL /// Protocol that a custom scalar should implement to be used with SwiftGraphQL. -public protocol GraphQLScalar: Encodable { +public protocol GraphQLScalar: Encodable, Sendable { /// A decoder from the any-type codable value. init(from: AnyCodable) throws @@ -20,7 +20,7 @@ extension Array: GraphQLScalar where Element: GraphQLScalar { switch(codable.value) { case let value as [AnyCodable]: self = try value.map { try Element(from: $0) } - case let value as [Any]: + case let value as [any Sendable]: // NOTE: We need this special case because wrapped scalar types (e.g. `[String]` or `String?`) are represented as a single `AnyCodable` value with a nested structure (e.g. `AnyCodable([String])`). self = try value.map { try Element(from: AnyCodable($0)) } default: @@ -185,7 +185,7 @@ extension AnyCodable: GraphQLScalar { // MARK: - Error -public enum ScalarDecodingError: Error { +public enum ScalarDecodingError: Error, @unchecked Sendable { /// Scalar expected a given type but received a value of a different type. case unexpectedScalarType(expected: String, received: Any) diff --git a/Sources/SwiftGraphQL/Selection/Operation.swift b/Sources/SwiftGraphQL/Selection/Operation.swift index e6a25c4e..556f436b 100644 --- a/Sources/SwiftGraphQL/Selection/Operation.swift +++ b/Sources/SwiftGraphQL/Selection/Operation.swift @@ -9,7 +9,7 @@ public protocol GraphQLOperation { static var operation: GraphQLOperationKind { get } } -public enum GraphQLOperationKind: String { +public enum GraphQLOperationKind: String, Sendable { case query case mutation case subscription diff --git a/Sources/SwiftGraphQL/Selection/Selection+Transform.swift b/Sources/SwiftGraphQL/Selection/Selection+Transform.swift index e31e6cf6..324d0f5d 100644 --- a/Sources/SwiftGraphQL/Selection/Selection+Transform.swift +++ b/Sources/SwiftGraphQL/Selection/Selection+Transform.swift @@ -14,7 +14,7 @@ public extension Selection { switch fields.__state { case let .decoding(data): switch data.value { - case let array as [Any]: + case let array as [any Sendable]: return try array.map { try self.__decode(data: AnyCodable($0)) } default: throw ObjectDecodingError.unexpectedObjectType(expected: "Array", received: data.value) @@ -85,7 +85,7 @@ public extension Selection { public extension Selection { /// Maps selection's return value into a new value using provided mapping function. - func map(_ fn: @escaping (T) -> MappedType) -> Selection { + func map(_ fn: @Sendable @escaping (T) -> MappedType) -> Selection { Selection { fields in let selection = self.__selection() fields.__select(selection) diff --git a/Sources/SwiftGraphQL/Selection/Selection.swift b/Sources/SwiftGraphQL/Selection/Selection.swift index b08b4782..f3682efc 100644 --- a/Sources/SwiftGraphQL/Selection/Selection.swift +++ b/Sources/SwiftGraphQL/Selection/Selection.swift @@ -3,25 +3,35 @@ import GraphQL /// Fields is a class that selection passes around to collect information about the selection /// of a query and also aids the decoding process. -public final class Fields { - +public final class Fields: @unchecked Sendable { + // Internal representation of selection. - private(set) var fields = [GraphQLField]() - + private let queue = DispatchQueue(label: "com.swiftgraphql.fields") + private var _fields = [GraphQLField]() + + var fields: [GraphQLField] { + get { queue.sync { _fields } } + set { queue.sync { _fields = newValue } } + } + /// State of the selection tells whether we are currently building up the query and mocking the /// response values or performing decoding with returned data. /// /// - NOTE: This variable should only be used by the generated code. - public private(set) var __state: State = .selecting - + private var _state: State = .selecting + public var __state: State { + get { queue.sync { _state } } + set { queue.sync { _state = newValue } } + } + public enum State { - + /// Selection is collection data about the query. case selecting - + /// Selection is trying to parse the received data into a desired value. case decoding(AnyCodable) - + /// Tells whether the fields have actual data or not. public var isMocking: Bool { switch self { @@ -57,9 +67,9 @@ public final class Fields { public func __select(_ fields: [GraphQLField]) { self.fields.append(contentsOf: fields) } - + // MARK: - Decoding - + /// Tries to decode a field from an object selection. /// /// - NOTE: This function should only be used by the generated code! @@ -67,7 +77,7 @@ public final class Fields { switch self.__state { case .decoding(let codable): switch codable.value { - case let dict as [String: Any]: + case let dict as [String: any Sendable]: // We replce `nil` values with Void AnyCodable values for // cleaner protocol declaration. return try decoder(AnyCodable(dict[field])) @@ -81,9 +91,9 @@ public final class Fields { throw ObjectDecodingError.decodingWhileSelecting } } - + // MARK: - Analysis - + /// Returns all types referenced in the fields. var types: [String] { self.fields.flatMap { $0.types }.unique(by: { $0 }) @@ -96,15 +106,15 @@ public final class Fields { /// fields a query should fetch. To do that, it passes around a Fields /// class reference. Generated code later calls `select` method on Fields /// to add a subfield to the selection. -public struct Selection { - +public struct Selection: Sendable { + /// Function that SwiftGraphQL uses to generate selection and convert received JSON /// structure into concrete Swift structure. - private var decoder: (Fields) throws -> T - + private var decoder: @Sendable (Fields) throws -> T + // MARK: - Initializer - - public init(decoder: @escaping (Fields) throws -> T) { + + public init(decoder: @Sendable @escaping (Fields) throws -> T) { self.decoder = decoder } @@ -115,14 +125,14 @@ public struct Selection { /// - NOTE: This is an internal function that should only be used by the generated code. public func __selection() -> [GraphQLField] { let fields = Fields() - + do { _ = try decoder(fields) } catch {} - + return fields.fields } - + /// Returns all types referenced in the selection. public var types: Set { self.__selection().map { $0.types }.reduce(Set(), { $0.union($1) }) @@ -136,7 +146,7 @@ public struct Selection { public func __decode(data: AnyCodable) throws -> T { // Construct a copy of the selection set, and use the new selection set to decode data. let fields = Fields(data: data) - + let data = try self.decoder(fields) return data } @@ -152,7 +162,7 @@ public struct Selection { // MARK: - Error -public enum ObjectDecodingError: Error, Equatable { +public enum ObjectDecodingError: Error, Equatable, @unchecked Sendable { public static func == (lhs: ObjectDecodingError, rhs: ObjectDecodingError) -> Bool { switch (lhs, rhs) { case (.unexpectedNilValue, .unexpectedNilValue): diff --git a/Sources/SwiftGraphQL/Serialization/OptionalArgument.swift b/Sources/SwiftGraphQL/Serialization/OptionalArgument.swift index 6ede56d1..3d358861 100644 --- a/Sources/SwiftGraphQL/Serialization/OptionalArgument.swift +++ b/Sources/SwiftGraphQL/Serialization/OptionalArgument.swift @@ -5,14 +5,14 @@ import GraphQL /// /// To prevent encoding of absent values we use a the OptionalArgumentProtocol /// in document parsing to figure out whether we should encode or skip the argument. -protocol OptionalArgumentProtocol { +protocol OptionalArgumentProtocol: Sendable { /// Tells whether an optional argument has a value. var hasValue: Bool { get } } /// OptionalArgument is a utility structure used to represent possibly absent values. -public struct OptionalArgument: OptionalArgumentProtocol { +public struct OptionalArgument: OptionalArgumentProtocol { /// Special structure that allows recursive type references. /// diff --git a/Sources/SwiftGraphQLCLI/main.swift b/Sources/SwiftGraphQLCLI/main.swift index 58f611b6..e5885eb3 100755 --- a/Sources/SwiftGraphQLCLI/main.swift +++ b/Sources/SwiftGraphQLCLI/main.swift @@ -31,7 +31,7 @@ struct SwiftGraphQLCLI: ParsableCommand { // MARK: - Configuration - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "swift-graphql" ) diff --git a/Sources/SwiftGraphQLClient/Client/Config.swift b/Sources/SwiftGraphQLClient/Client/Config.swift index 3ac415c7..4896a696 100644 --- a/Sources/SwiftGraphQLClient/Client/Config.swift +++ b/Sources/SwiftGraphQLClient/Client/Config.swift @@ -2,10 +2,10 @@ import Foundation import Logging /// A structure that lets you configure -public class ClientConfiguration { +public final class ClientConfiguration: @unchecked Sendable { /// Logger that we use to communitcate state changes and events inside the client. - open var logger: Logger = Logger(label: "graphql.client") + public var logger: Logger = Logger(label: "graphql.client") public init() { // Certain built-in exchanges (e.g. `DebugExchange`) product `.debug` logs that require `.debug` log level to be visible. This makes sure that the expected functionality of all exchanges matches the actual functionality (e.g. "debug exchange actually prints messages in the console"). diff --git a/Sources/SwiftGraphQLClient/Client/Core.swift b/Sources/SwiftGraphQLClient/Client/Core.swift index d32cc150..05cefcf8 100644 --- a/Sources/SwiftGraphQLClient/Client/Core.swift +++ b/Sources/SwiftGraphQLClient/Client/Core.swift @@ -1,4 +1,4 @@ -import Combine +@preconcurrency import Combine import Foundation import GraphQL import Logging @@ -7,36 +7,75 @@ import Logging /// /// - NOTE: SwiftUI bindings and Selection interloop aren't bound to the default implementation. /// You may use them with a custom implementation as well. -public class Client: GraphQLClient, ObservableObject { - +public final class Client: GraphQLClient, ObservableObject { + /// Request to use to perform operations. public let request: URLRequest - + /// A configuration for the client behaviour. private let config: ClientConfiguration - + + /// Lock for protecting mutable state + private let lock = NSLock() + // MARK: - Exchange Pipeline - + /// The operations stream that lets the client send and listen for them. - private var operations = PassthroughSubject() - + nonisolated(unsafe) private let operations = PassthroughSubject() + /// Stream of results that may be used as the base for sources. - private var results: AnyPublisher - + nonisolated(unsafe) private var _results: AnyPublisher + nonisolated(unsafe) private var results: AnyPublisher { + get { + lock.lock() + defer { lock.unlock() } + return _results + } + set { + lock.lock() + defer { lock.unlock() } + _results = newValue + } + } + // MARK: - Sources - + /// Stream of results related to a given operation. public typealias Source = AnyPublisher - + /// Map of currently active sources identified by their operation identifier. /// /// - NOTE: Removing the source from the active list should start its deallocation. - private var active: [String: Source] - - private var cancellables = Set() - + nonisolated(unsafe) private var _active: [String: Source] + nonisolated(unsafe) private var active: [String: Source] { + get { + lock.lock() + defer { lock.unlock() } + return _active + } + set { + lock.lock() + defer { lock.unlock() } + _active = newValue + } + } + + nonisolated(unsafe) private var _cancellables = Set() + nonisolated(unsafe) private var cancellables: Set { + get { + lock.lock() + defer { lock.unlock() } + return _cancellables + } + set { + lock.lock() + defer { lock.unlock() } + _cancellables = newValue + } + } + // MARK: - Initializer - + /// Creates a new client that processes requests using provided exchanges. /// /// - parameter exchanges: List of exchanges that process each operation left-to-right. @@ -48,33 +87,35 @@ public class Client: GraphQLClient, ObservableObject { ) { // A publisher that never emits anything. let noop = Empty().eraseToAnyPublisher() - + self.request = request self.config = config - self.results = noop - self.active = [:] - + self._results = noop + self._active = [:] + self._cancellables = Set() + let operations = operations.share().eraseToAnyPublisher() - + // We think of all exchanges as a single flattened exchange - once // we have sent a request through the pipeline there's nothing left to do // and we pass it in the stream of all operations from the client. let exchange = ComposeExchange(exchanges: exchanges) - self.results = exchange + self._results = + exchange .register(client: self, operations: operations, next: { _ in noop }) .share() .eraseToAnyPublisher() - + // We start the chain to make sure the data is always flowing through the pipeline. // This is important to make sure all exchanges are fully initialised // even if there are no active subscribers yet. self.results .sink { _ in } - .store(in: &self.cancellables) - + .store(in: &self._cancellables) + self.config.logger.info("GraphQL Client ready!") } - + /// Creates a new GraphQL Client using default exchanges, ready to go and be used. /// /// - NOTE: By default, it includes deduplication exchange, basic caching exchange and the fetch exchange. @@ -82,19 +123,19 @@ public class Client: GraphQLClient, ObservableObject { let exchanges: [Exchange] = [ DedupExchange(), CacheExchange(), - FetchExchange() + FetchExchange(), ] - + self.init(request: request, exchanges: exchanges, config: config) } - + // MARK: - Core - + /// Log a debug message. public var logger: Logger { self.config.logger } - + /// Reexecutes an operation by sending it down the exchange pipeline. /// /// - NOTE: The operation only re-executes if there are any active subscribers @@ -105,20 +146,20 @@ public class Client: GraphQLClient, ObservableObject { self.config.logger.debug("Operation \(operation.id) is no longer active.") return } - + self.config.logger.debug("Reexecuting operation \(operation.id)...") self.operations.send(operation) } - + /// Executes an operation by sending it down the exchange pipeline. public func execute(operation: Operation) -> Source { self.config.logger.debug("Execution operation \(operation.id)...") - + // Mutations shouldn't have open sources because they are executed once and "discarded". if operation.kind == .mutation { return createResultSource(operation: operation) } - + let source: Source if let existingSource = active[operation.id] { source = existingSource @@ -126,7 +167,7 @@ public class Client: GraphQLClient, ObservableObject { source = createResultSource(operation: operation) active[operation.id] = source } - + // We chain the `receiveOperation` event outside of the `createResultSource` // to send a new operation down the exchange chain even if the // source already exists. @@ -143,33 +184,35 @@ public class Client: GraphQLClient, ObservableObject { // response immediately or wait for the result to come back. // // To sum up both cases, client shouldn't handle processing of the operations. - return source + return + source .handleEvents(receiveSubscription: { _ in self.operations.send(operation) }) .eraseToAnyPublisher() } - + /// Returns a new result source that private func createResultSource(operation: Operation) -> Source { self.config.logger.debug("Creating result source for operation \(operation.id)...") - + let source = self.results .filter { $0.operation.kind == operation.kind && $0.operation.id == operation.id } .eraseToAnyPublisher() - + // We aren't interested in composing a full-blown // pipeline for mutations because we only get a single result // (i.e. the result of the mutation). if operation.kind == .mutation { - return source + return + source .handleEvents(receiveSubscription: { _ in self.operations.send(operation) }) .first() .eraseToAnyPublisher() } - + // We create a new source that listenes for events until // a teardown event is sent through the pipeline. When that // happens, we emit a completion event. @@ -180,25 +223,26 @@ public class Client: GraphQLClient, ObservableObject { let torndown = self.operations .filter { $0.kind == .teardown && $0.id == operation.id } .eraseToAnyPublisher() - - let result: AnyPublisher = source + + let result: AnyPublisher = + source .handleEvents(receiveCompletion: { _ in // Once the publisher stops the stream (i.e. the stream ended because we // received all relevant results), we dismantle the pipeline by sending // the teardown event to all exchanges in the chain. self.config.logger.debug("Operation \(operation.id) source has completed.") - + self.active.removeValue(forKey: operation.id) self.operations.send(operation.with(kind: .teardown)) }) .map { result -> AnyPublisher in self.config.logger.debug("Processing result of operation \(operation.id)") - + // Mark a result as stale when a new operation is sent with the same key. guard operation.kind == .query else { return Just(result).eraseToAnyPublisher() } - + // Mark the current result as `stale` when the client // requests a query with the same key again. let staleResult: AnyPublisher = self.operations @@ -210,7 +254,7 @@ public class Client: GraphQLClient, ObservableObject { return copy } .eraseToAnyPublisher() - + return Just(result).merge(with: staleResult).eraseToAnyPublisher() } .switchToLatest() @@ -223,7 +267,7 @@ public class Client: GraphQLClient, ObservableObject { // Once the source has been canceled because the application is no longer interested // in results, we start the teardown process. self.config.logger.debug("Operation \(operation.id) source has been canceled.") - + self.active.removeValue(forKey: operation.id) self.operations.send(operation.with(kind: .teardown)) }) @@ -232,12 +276,12 @@ public class Client: GraphQLClient, ObservableObject { // but a different subscriber. .share() .eraseToAnyPublisher() - + return result } - + // MARK: - Querying - + /// Executes a query request with given execution parameters. public func query( _ args: ExecutionArgs, @@ -252,7 +296,7 @@ public class Client: GraphQLClient, ObservableObject { types: [], args: args ) - + return self.execute(operation: operation) } @@ -265,7 +309,7 @@ public class Client: GraphQLClient, ObservableObject { /// /// Additionally, due to the differences between async/await and /// Combine publishers, the async APIs will only return a single value, - /// even if the query is invalidated. Therefore if you currently + /// even if the query is invalidated. Therefore if you currently /// rely on invalidation behaviour provided by publishers we suggest /// you continue to use the Combine APIs. public func query( @@ -289,7 +333,7 @@ public class Client: GraphQLClient, ObservableObject { types: [], args: args ) - + return self.execute(operation: operation) } @@ -320,7 +364,7 @@ public class Client: GraphQLClient, ObservableObject { types: [], args: args ) - + return self.execute(operation: operation) } } diff --git a/Sources/SwiftGraphQLClient/Client/Operation.swift b/Sources/SwiftGraphQLClient/Client/Operation.swift index 6e800716..71e520da 100644 --- a/Sources/SwiftGraphQLClient/Client/Operation.swift +++ b/Sources/SwiftGraphQLClient/Client/Operation.swift @@ -3,7 +3,7 @@ import Foundation import GraphQL /// Operation describes a single request that may be processed by multiple exchange along the chain. -public struct Operation: Identifiable, Equatable, Hashable { +public struct Operation: Identifiable, Equatable, Hashable, Sendable { public init(id: String, kind: Operation.Kind, request: URLRequest, policy: Operation.Policy, types: [String], args: ExecutionArgs) { self.id = id self.kind = kind @@ -19,7 +19,7 @@ public struct Operation: Identifiable, Equatable, Hashable { /// Identifies the operation type. public var kind: Kind - public enum Kind: String { + public enum Kind: String, Sendable { case query case mutation case subscription @@ -34,7 +34,7 @@ public struct Operation: Identifiable, Equatable, Hashable { /// Specifies the caching-networking mechanism that exchanges should follow. public var policy: Policy - public enum Policy: String { + public enum Policy: String, Sendable { /// Prefers cached results and falls back to sending an API request when there are no prior results. case cacheFirst = "cache-first" @@ -84,7 +84,7 @@ extension Operation { } /// A structure describing the result of an operation execution. -public struct OperationResult: Equatable { +public struct OperationResult: Equatable, Sendable { /// Back-reference to the operation that triggered the execution. public var operation: Operation @@ -114,7 +114,7 @@ public struct OperationResult: Equatable { /// An error structure describing an error that may have happened in one of the exchanges. -public enum CombinedError: Error { +public enum CombinedError: Error, Sendable { /// Describes an error that occured on the networking layer. case network(URLError) @@ -161,7 +161,7 @@ extension OperationResult: Identifiable { /// /// - NOTE: Decoded result may include errors from invalid data even if /// the response query was correct. -public struct DecodedOperationResult { +public struct DecodedOperationResult: Sendable { /// Back-reference to the operation that triggered the execution. public var operation: Operation diff --git a/Sources/SwiftGraphQLClient/Client/Spec.swift b/Sources/SwiftGraphQLClient/Client/Spec.swift index ad809df5..4ca109f9 100644 --- a/Sources/SwiftGraphQLClient/Client/Spec.swift +++ b/Sources/SwiftGraphQLClient/Client/Spec.swift @@ -4,7 +4,7 @@ import Logging /// Specifies the minimum requirements of a client to support the execution of queries /// composed using SwiftGraphQL. -public protocol GraphQLClient { +public protocol GraphQLClient: Sendable { /// Request to use to perform the operation. var request: URLRequest { get } diff --git a/Sources/SwiftGraphQLClient/Exchanges/CacheExchange.swift b/Sources/SwiftGraphQLClient/Exchanges/CacheExchange.swift index 87002f31..10c89403 100644 --- a/Sources/SwiftGraphQLClient/Exchanges/CacheExchange.swift +++ b/Sources/SwiftGraphQLClient/Exchanges/CacheExchange.swift @@ -10,127 +10,146 @@ import Foundation /// - NOTE: Cache exchange doesn't perform any deduplication of requests. /// /// - NOTE: The caching pattern used here is greedy and not optimal. -public class CacheExchange: Exchange { - +public class CacheExchange: Exchange, @unchecked Sendable { + /// Results from previous oeprations indexed by operation's ids. private var resultCache: [String: OperationResult] - + /// Index of operation IDs indexed by the typename in their result. private var operationCache: [String: Set] - + + /// Serial queue for synchronizing access to caches + private let queue: DispatchQueue + public init() { self.resultCache = [:] self.operationCache = [:] + self.queue = DispatchQueue(label: "com.swiftgraphql.cacheexchange") } - + /// Clears all cached results. /// /// - NOTE: This method doesn't re-execute any of the watched queries. public func clear() { - self.resultCache = [:] - self.operationCache = [:] + queue.sync { + self.resultCache = [:] + self.operationCache = [:] + } } - + /// Tells whether a given operation should rely on the result saved in the cache. /// /// - NOTE: CacheOnly operations might get a nil value and fail when selection tries /// to decode them. That's O.K. private func shouldUseCache(operation: Operation) -> Bool { - operation.kind == .query && - operation.policy != .networkOnly && - (operation.policy == .cacheOnly || resultCache[operation.id] != nil) + queue.sync { + operation.kind == .query && operation.policy != .networkOnly && (operation.policy == .cacheOnly || resultCache[operation.id] != nil) + } } - + public func register( client: GraphQLClient, operations: AnyPublisher, next: @escaping ExchangeIO ) -> AnyPublisher { let shared = operations.share() - + // We synchronously send cached results upstream. - let cachedOps: AnyPublisher = shared - .compactMap { operation in - guard self.shouldUseCache(operation: operation) else { - return nil - } - - let cachedResult = self.resultCache[operation.id] - if operation.policy == .cacheAndNetwork { - return cachedResult?.with(stale: true) + let cachedOps: AnyPublisher = + shared + .compactMap { [weak self] operation in + guard let self = self else { return nil } + return self.queue.sync { + let shouldUseCache = operation.kind == .query && operation.policy != .networkOnly && (operation.policy == .cacheOnly || self.resultCache[operation.id] != nil) + + guard shouldUseCache else { + return nil + } + + let cachedResult = self.resultCache[operation.id] + if operation.policy == .cacheAndNetwork { + return cachedResult?.with(stale: true) + } + return cachedResult } - return cachedResult } .eraseToAnyPublisher() - + // We filter requests that hit cache and listen for results // to keep track of received results. - let downstream = shared - .filter({ operation in - // Cache stops cache-only operations - they shouldn't reach any - // other exchange for obvious reasons. - guard operation.policy != .cacheOnly else { - return false - } - - // We only cache queries and ignore all other kinds of transactions. - guard operation.kind == .query else { - return true + let downstream = + shared + .filter({ [weak self] operation in + guard let self = self else { return false } + return self.queue.sync { + // Cache stops cache-only operations - they shouldn't reach any + // other exchange for obvious reasons. + guard operation.policy != .cacheOnly else { + return false + } + + // We only cache queries and ignore all other kinds of transactions. + guard operation.kind == .query else { + return true + } + + // Filter out cache-first requests that were matched/hit. + let wasHit = operation.policy == .cacheFirst && self.resultCache[operation.id] != nil + return operation.policy != .cacheFirst || !wasHit } - - // Filter out cache-first requests that were matched/hit. - let wasHit = operation.policy == .cacheFirst && self.resultCache[operation.id] != nil - return operation.policy != .cacheFirst || !wasHit }) .eraseToAnyPublisher() - + let forwardedOps: AnyPublisher = next(downstream) - - let upstream = forwardedOps - .handleEvents(receiveOutput: { result in - // Invalidate the cache given a mutation's response. - if result.operation.kind == .mutation { - var pendingOperations = Set() - - // Collect all operations that need to be invalidated. - for typename in result.operation.types { - guard let cachedOperations = self.operationCache[typename] else { - continue + + let upstream = + forwardedOps + .handleEvents(receiveOutput: { [weak self] result in + guard let self = self else { return } + self.queue.sync { + // Invalidate the cache given a mutation's response. + if result.operation.kind == .mutation { + var pendingOperations = Set() + + // Collect all operations that need to be invalidated. + for typename in result.operation.types { + guard let cachedOperations = self.operationCache[typename] else { + continue + } + cachedOperations.forEach { pendingOperations.insert($0) } } - cachedOperations.forEach { pendingOperations.insert($0) } - } - - // Invalidate all operations that need invalidation. - for opid in pendingOperations { - guard let cachedResult = self.resultCache[opid] else { - return + + // Invalidate all operations that need invalidation. + for opid in pendingOperations { + guard let cachedResult = self.resultCache[opid] else { + return + } + + self.resultCache.removeValue(forKey: opid) + client.reexecute(operation: cachedResult.operation.with(policy: .networkOnly)) } - - self.resultCache.removeValue(forKey: opid) - client.reexecute(operation: cachedResult.operation.with(policy: .networkOnly)) } - } - - // Cache query result and operation references. - // (AnyCodable represents nil values as Void objects.) - if result.operation.kind == .query, result.error == nil { - self.resultCache[result.operation.id] = result - - // NOTE: cache-only operations never receive data from the - // exchanges coming after the cache (i.e. from the upstream stream) - // meaning they are never indexed for re-execution. - for typename in result.operation.types { - if self.operationCache[typename] == nil { - self.operationCache[typename] = Set() + + // Cache query result and operation references. + // (AnyCodable represents nil values as Void objects.) + if result.operation.kind == .query, result.error == nil { + self.resultCache[result.operation.id] = result + + // NOTE: cache-only operations never receive data from the + // exchanges coming after the cache (i.e. from the upstream stream) + // meaning they are never indexed for re-execution. + for typename in result.operation.types { + if self.operationCache[typename] == nil { + self.operationCache[typename] = Set() + } + + self.operationCache[typename]!.insert(result.operation.id) } - - self.operationCache[typename]!.insert(result.operation.id) } } }) .eraseToAnyPublisher() - - + return cachedOps.merge(with: upstream).eraseToAnyPublisher() } } diff --git a/Sources/SwiftGraphQLClient/Exchanges/ErrorExchange.swift b/Sources/SwiftGraphQLClient/Exchanges/ErrorExchange.swift index 8c0042dd..256cb3fa 100644 --- a/Sources/SwiftGraphQLClient/Exchanges/ErrorExchange.swift +++ b/Sources/SwiftGraphQLClient/Exchanges/ErrorExchange.swift @@ -5,9 +5,9 @@ import Foundation public struct ErrorExchange: Exchange { /// Callback function that the exchange calls for every error in the operation result. - private var onError: (CombinedError, Operation) -> Void + private var onError: @Sendable (CombinedError, Operation) -> Void - public init(onError: @escaping (CombinedError, Operation) -> Void) { + public init(onError: @Sendable @escaping (CombinedError, Operation) -> Void) { self.onError = onError } diff --git a/Sources/SwiftGraphQLClient/Exchanges/ExtensionsExchange.swift b/Sources/SwiftGraphQLClient/Exchanges/ExtensionsExchange.swift index 271bd438..b0470bc1 100644 --- a/Sources/SwiftGraphQLClient/Exchanges/ExtensionsExchange.swift +++ b/Sources/SwiftGraphQLClient/Exchanges/ExtensionsExchange.swift @@ -6,7 +6,7 @@ import GraphQL /// /// You should place this exchange before the asynchronous exchanges that perform requests /// (e.g. `FetchExchange`) so that the opeartion is modified before being sent. -public struct ExtensionsExchange: Exchange { +public struct ExtensionsExchange: @preconcurrency Exchange { /// Getter function called to get the extensions of an operation. private var getExtensions: (Operation) -> [String: AnyCodable]? diff --git a/Sources/SwiftGraphQLClient/Exchanges/WebSocketExchange.swift b/Sources/SwiftGraphQLClient/Exchanges/WebSocketExchange.swift index 58c31aa2..ab567ae6 100644 --- a/Sources/SwiftGraphQLClient/Exchanges/WebSocketExchange.swift +++ b/Sources/SwiftGraphQLClient/Exchanges/WebSocketExchange.swift @@ -9,7 +9,7 @@ import GraphQLWebSocket /// /// - NOTE: By default WebSocketExchange only handles subscription operations /// but you may configure it to handle all operations equally. -public class WebSocketExchange: Exchange { +public class WebSocketExchange: @preconcurrency Exchange { /// Reference to the client that actually establishes the WebSocket connection with the server. private var client: GraphQLWebSocket diff --git a/Sources/SwiftGraphQLClient/Extensions/Publishers+Extensions.swift b/Sources/SwiftGraphQLClient/Extensions/Publishers+Extensions.swift index eefeecc9..4f2ed88f 100644 --- a/Sources/SwiftGraphQLClient/Extensions/Publishers+Extensions.swift +++ b/Sources/SwiftGraphQLClient/Extensions/Publishers+Extensions.swift @@ -3,14 +3,14 @@ import Foundation // MARK: - TakeUntil Publisher -extension Publisher { +extension Publisher where Output: Sendable { /// Takes upstream values until predicates a value. public func takeUntil(_ terminator: Terminator) -> Publishers.TakenUntilPublisher { Publishers.TakenUntilPublisher(upstream: self, terminator: terminator) } /// Takes the first emitted value and completes or throws an error - func first() async throws -> Output { + nonisolated func first() async throws -> Output { try await withCheckedThrowingContinuation { continuation in var cancellable: AnyCancellable? @@ -23,28 +23,28 @@ extension Publisher { continuation.resume(throwing: error) } cancellable?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) + } receiveValue: { @Sendable value in + continuation.resume(returning: value) } } } } -extension Publisher where Failure == Never { +extension Publisher where Failure == Never, Output: Sendable { /// Takes the first emitted value and completes or throws an error /// /// Note: While this behaves much the same as the published-based /// APIs, async/await inherently does __not__ support multiple /// return values. If you expect multiple values from an async/await /// API, please use the corresponding publisher API instead. - func first() async -> Output { + nonisolated func first() async -> Output { await withCheckedContinuation { continuation in var cancellable: AnyCancellable? cancellable = first() .sink { _ in cancellable?.cancel() - } receiveValue: { value in + } receiveValue: { @Sendable value in continuation.resume(with: .success(value)) } } @@ -52,63 +52,63 @@ extension Publisher where Failure == Never { } extension Publishers { - + /// A subscriber that takes upstream values until terminator emits a value. public struct TakenUntilPublisher: Publisher { public typealias Output = Upstream.Output public typealias Failure = Upstream.Failure - + /// A function that tells whether the condition is met. private var terminator: Terminator - + /// Publisher emitting the values. private var upstream: Upstream - + // MARK: - Initializer - + public init(upstream: Upstream, terminator: Terminator) { self.upstream = upstream self.terminator = terminator } - + // MARK: - Publisher - - public func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { - + + public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { + // Wraps the actual subscriber inside a custom subscribers // and subscribes to the upstream publisher with it. let takeUntilSubscription = Subscriptions.TakeUntilSubscription( subscriber: subscriber, terminator: self.terminator ) - + subscriber.receive(subscription: takeUntilSubscription) self.upstream.receive(subscriber: takeUntilSubscription) } - + } } extension Subscriptions { - + /// A subscription that emits upstream values downstream until predicate emits a value. final class TakeUntilSubscription: Subscription, Subscriber { - + typealias Input = Downstream.Input typealias Failure = Downstream.Failure - + /// Subscription that yields values streamed to the subscriber. private var subscription: Subscription? - + /// Tells how much events downstream has already requested from the upstream. private var demand: Subscribers.Demand - + /// The subscriber we are forwarding values to. private var subscriber: Downstream - + /// Function telling whether the sought condition has been met. private var terminator: Terminator - + /// Cancellable reference to the terminator sink. private var cancellable: AnyCancellable? @@ -117,27 +117,29 @@ extension Subscriptions { self.terminator = terminator self.demand = .none } - + // MARK: - Subscriber - + /// Tells the subscriber that it has successfully subscribed to the publisher and may request items. func receive(subscription: Subscription) { self.subscription = subscription - + self.cancellable = self.terminator - .sink(receiveCompletion: { _ in - - }, receiveValue: { [weak self] _ in - guard let _ = self?.cancellable else { - return - } + .sink( + receiveCompletion: { _ in + + }, + receiveValue: { [weak self] _ in + guard self?.cancellable != nil else { + return + } + + // We send the completion event downstream and cancel the + // upstream subscription once we've received any event from the predicate. + self?.subscriber.receive(completion: .finished) + self?.cancel() + }) - // We send the completion event downstream and cancel the - // upstream subscription once we've received any event from the predicate. - self?.subscriber.receive(completion: .finished) - self?.cancel() - }) - // Request the accumulated demand and drain it. if self.demand > 0 { self.subscription?.request(self.demand) @@ -146,30 +148,30 @@ extension Subscriptions { self.demand = .none } } - + func receive(_ input: Downstream.Input) -> Subscribers.Demand { self.subscriber.receive(input) } - + func receive(completion: Subscribers.Completion) { self.subscriber.receive(completion: completion) } - + // MARK: - Subscription - + func request(_ demand: Subscribers.Demand) { guard let subscription = subscription else { self.demand += demand return } - + subscription.request(demand) } - + func cancel() { self.subscription?.cancel() self.subscription = nil - + self.cancellable?.cancel() self.cancellable = nil } diff --git a/Sources/SwiftGraphQLCodegen/Extensions/String+Extensions.swift b/Sources/SwiftGraphQLCodegen/Extensions/String+Extensions.swift index 3395a6f8..5d7ff22e 100644 --- a/Sources/SwiftGraphQLCodegen/Extensions/String+Extensions.swift +++ b/Sources/SwiftGraphQLCodegen/Extensions/String+Extensions.swift @@ -1,6 +1,5 @@ import Foundation import SwiftFormat -import SwiftFormatConfiguration extension String { /// Formats the given Swift source code. @@ -17,7 +16,7 @@ extension String { var output = "" do { - try formatter.format(source: trimmed, assumingFileURL: nil, to: &output) + try formatter.format(source: trimmed, assumingFileURL: nil, selection: .infinite, to: &output) } catch(let err) { throw CodegenError.formatting(err) } diff --git a/Sources/SwiftGraphQLCodegen/Generator.swift b/Sources/SwiftGraphQLCodegen/Generator.swift index 237b0122..4edeeafe 100644 --- a/Sources/SwiftGraphQLCodegen/Generator.swift +++ b/Sources/SwiftGraphQLCodegen/Generator.swift @@ -3,7 +3,7 @@ import GraphQLAST /// Structure that holds methods for SwiftGraphQL query-builder generation. public struct GraphQLCodegen { - + /// Map of supported scalars. private let scalars: ScalarMap @@ -14,7 +14,7 @@ public struct GraphQLCodegen { } // MARK: - Methods - + /// Generates Swift files for the graph selections /// - Parameters: /// - schema: The GraphQL schema @@ -23,7 +23,7 @@ public struct GraphQLCodegen { /// - Returns: A list of generated files public func generate(schema: Schema, generateStaticFields: Bool, singleFile: Bool = false) throws -> [GeneratedFile] { let context = Context(schema: schema, scalars: self.scalars) - + let subscription = schema.operations.first { $0.isSubscription }?.type.name let objects = schema.objects let operations = schema.operations.map { $0.declaration() } @@ -31,29 +31,29 @@ public struct GraphQLCodegen { var files: [GeneratedFile] = [] let header = """ - // This file was auto-generated using maticzav/swift-graphql. DO NOT EDIT MANUALLY! - import Foundation - import GraphQL - import SwiftGraphQL - """ + // This file was auto-generated using maticzav/swift-graphql. DO NOT EDIT MANUALLY! + import Foundation + import GraphQL + import SwiftGraphQL + """ let graphContents = """ - public enum Operations {} - \(operations.lines) + public enum Operations {} + \(operations.lines) + + public enum Objects {} - public enum Objects {} + public enum Interfaces {} - public enum Interfaces {} + public enum Unions {} - public enum Unions {} + public enum Enums {} - public enum Enums {} + /// Utility pointer to InputObjects. + public typealias Inputs = InputObjects - /// Utility pointer to InputObjects. - public typealias Inputs = InputObjects - - public enum InputObjects {} - """ + public enum InputObjects {} + """ func addFile(name: String, contents: String) throws { let fileContents: String diff --git a/Sources/SwiftGraphQLCodegen/Generator/Enum.swift b/Sources/SwiftGraphQLCodegen/Generator/Enum.swift index 24f58ba0..cd3b6cee 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/Enum.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/Enum.swift @@ -9,7 +9,7 @@ extension EnumType { """ extension Enums { \(docs) - public enum \(name.pascalCase): String, CaseIterable, Codable { + public enum \(name.pascalCase): String, CaseIterable, Codable, Sendable { \(values) } } @@ -58,7 +58,7 @@ extension EnumType { /// Mock value declaration. private var mock: String { let value = self.enumValues.first! - return "public static var mockValue = Self.\(value.name.camelCasePreservingSurroundingUnderscores.normalize)" + return "public static let mockValue = Self.\(value.name.camelCasePreservingSurroundingUnderscores.normalize)" } } diff --git a/Sources/SwiftGraphQLCodegen/Generator/InputObject.swift b/Sources/SwiftGraphQLCodegen/Generator/InputObject.swift index 71e3b640..f17805f7 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/InputObject.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/InputObject.swift @@ -10,7 +10,7 @@ extension InputObjectType { func declaration(context: Context) throws -> String { return """ extension InputObjects { - public struct \(self.name.pascalCase): Encodable, Hashable { + public struct \(self.name.pascalCase): Encodable, Hashable, Sendable { \(try self.inputFields.map { try $0.declaration(context: context) }.joined(separator: "\n")) diff --git a/Sources/SwiftGraphQLCodegen/Generator/Interface.swift b/Sources/SwiftGraphQLCodegen/Generator/Interface.swift index d5d79236..2d0800f4 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/Interface.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/Interface.swift @@ -11,7 +11,7 @@ import SwiftGraphQLUtils extension InterfaceType: Structure {} extension InterfaceType { - + /// Returns a code that represents an interface. /// /// - parameter objects: List of all objects in the schema. @@ -20,19 +20,19 @@ extension InterfaceType { let fields = try self.fields.getDynamicSelections(parent: self.name, context: context) return """ - extension Interfaces { - public struct \(name) {} - } + extension Interfaces { + public struct \(name): Sendable {} + } - extension Fields where TypeLock == Interfaces.\(name) { - \(fields) - } + extension Fields where TypeLock == Interfaces.\(name) { + \(fields) + } - \(possibleTypes.selection(name: "Interfaces.\(name)", objects: objects)) + \(possibleTypes.selection(name: "Interfaces.\(name)", objects: objects)) - extension Selection where T == Never, TypeLock == Never { - public typealias \(name) = Selection - } - """ + extension Selection where T == Never, TypeLock == Never { + public typealias \(name) = Selection + } + """ } } diff --git a/Sources/SwiftGraphQLCodegen/Generator/Object.swift b/Sources/SwiftGraphQLCodegen/Generator/Object.swift index 30968eb3..02d22d5c 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/Object.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/Object.swift @@ -9,7 +9,7 @@ extension ObjectType: Structure { } extension ObjectType { - + /// Creates deifnitions used by SwiftGraphQL to make selection and decode a particular object. /// /// - parameter objects: All objects in the schema. @@ -17,45 +17,46 @@ extension ObjectType { func declaration(objects: [ObjectType], context: Context, alias: Bool = true) throws -> String { let apiName = self.name.pascalCase let selection = try self.fields.getDynamicSelections(parent: self.name, context: context) - + var code = """ - extension Objects { - public struct \(apiName) {} - } + extension Objects { + public struct \(apiName): Sendable {} + } + + extension Fields where TypeLock == Objects.\(apiName) { + \(selection) + } + + """ - extension Fields where TypeLock == Objects.\(apiName) { - \(selection) - } - - """ - guard alias else { return code } - + // Adds utility alias for the selection. - code.append(""" - extension Selection where T == Never, TypeLock == Never { - public typealias \(apiName) = Selection - } - """) - + code.append( + """ + extension Selection where T == Never, TypeLock == Never { + public typealias \(apiName) = Selection + } + """) + return code } - + /// Generates utility code that may be used to select a single field from the object using a static function. /// /// - parameter alias: Tells whether the code should include utility reference in `Selection.Type`. func statics(context: Context) throws -> String { let name = self.name.pascalCase let selections = try self.fields.getStaticSelections(for: self, context: context) - + let code = """ - extension Objects.\(name) { - \(selections) - } - """ - + extension Objects.\(name) { + \(selections) + } + """ + return code } } diff --git a/Sources/SwiftGraphQLCodegen/Generator/Union.swift b/Sources/SwiftGraphQLCodegen/Generator/Union.swift index 6055baae..0d9a8fbe 100644 --- a/Sources/SwiftGraphQLCodegen/Generator/Union.swift +++ b/Sources/SwiftGraphQLCodegen/Generator/Union.swift @@ -9,28 +9,28 @@ import SwiftGraphQLUtils extension UnionType: Structure { var fields: [Field] { - [] // Unions come with no predefined fields. + [] // Unions come with no predefined fields. } } extension UnionType { - + /// Returns a declaration of the union type that we add to the generated file. /// - parameter objects: func declaration(objects: [ObjectType], context: Context) throws -> String { let name = self.name.pascalCase let selections = possibleTypes.selection(name: "Unions.\(name)", objects: objects) - + return """ - extension Unions { - public struct \(name) {} - } + extension Unions { + public struct \(name): Sendable {} + } - \(selections) + \(selections) - extension Selection where T == Never, TypeLock == Never { - public typealias \(name) = Selection - } - """ + extension Selection where T == Never, TypeLock == Never { + public typealias \(name) = Selection + } + """ } } diff --git a/Tests/GraphQLWebSocketTests/ClientTests.swift b/Tests/GraphQLWebSocketTests/ClientTests.swift index da463ed7..66a80416 100644 --- a/Tests/GraphQLWebSocketTests/ClientTests.swift +++ b/Tests/GraphQLWebSocketTests/ClientTests.swift @@ -6,7 +6,7 @@ import XCTest @available(macOS 12, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -final class ClientTests: XCTestCase { +@MainActor final class ClientTests: XCTestCase { private var cancellables = Set() diff --git a/Tests/SwiftGraphQLClientTests/Exchanges/CacheExchangeTests.swift b/Tests/SwiftGraphQLClientTests/Exchanges/CacheExchangeTests.swift index f26f0524..097ed8bf 100644 --- a/Tests/SwiftGraphQLClientTests/Exchanges/CacheExchangeTests.swift +++ b/Tests/SwiftGraphQLClientTests/Exchanges/CacheExchangeTests.swift @@ -1,65 +1,83 @@ import Combine -import GraphQL import Foundation -@testable import SwiftGraphQLClient +import GraphQL import XCTest +@testable import SwiftGraphQLClient + final class CacheExchangeTests: XCTestCase { - + private var cancellables = Set() - + /// Function that executes desired operations in prepared environment and returns the trace. func environment( _ fn: (PassthroughSubject, PassthroughSubject) -> Void - ) -> [String] { - var trace: [String] = [] - - let operations = PassthroughSubject() - let results = PassthroughSubject() - - let client = MockClient(customReexecute: { operation in - trace.append("reexecuted: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") - }) - - - let exchange = CacheExchange() - exchange.register( - client: client, - operations: operations - .handleEvents(receiveOutput: { operation in - trace.append("requested: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") - }) - .eraseToAnyPublisher() - ) { ops in - let downstream = ops - .handleEvents(receiveOutput: { operation in - trace.append("forwarded: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") - }) - .compactMap({ op in - SwiftGraphQLClient.OperationResult?.none - }) - .eraseToAnyPublisher() - - let upstream = downstream - .merge(with: results.eraseToAnyPublisher()) - .eraseToAnyPublisher() - - return upstream - } - .sink { result in - let op = result.operation - let stale = result.stale ?? false - let value = result.data - - trace.append("resulted (\(stale)): \(op.id) \(value) (\(op.kind.rawValue))") + ) async -> [String] { + await withCheckedContinuation { continuation in + let operations = PassthroughSubject() + let results = PassthroughSubject() + + let queue = DispatchQueue(label: "com.swiftgraphql.test.trace") + var trace: [String] = [] + + let client = MockClient(customReexecute: { operation in + queue.sync { + trace.append("reexecuted: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") + } + }) + + let exchange = CacheExchange() + let completion = exchange.register( + client: client, + operations: + operations + .handleEvents(receiveOutput: { operation in + queue.sync { + trace.append("requested: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") + } + }) + .eraseToAnyPublisher() + ) { ops in + let downstream = + ops + .handleEvents(receiveOutput: { operation in + queue.sync { + trace.append("forwarded: \(operation.id) (\(operation.kind.rawValue), \(operation.policy.rawValue))") + } + }) + .compactMap({ op in + SwiftGraphQLClient.OperationResult?.none + }) + .eraseToAnyPublisher() + + let upstream = + downstream + .merge(with: results.eraseToAnyPublisher()) + .eraseToAnyPublisher() + + return upstream + } + .handleEvents(receiveCompletion: { _ in + continuation.resume(returning: trace) + }) + .sink { result in + let op = result.operation + let stale = result.stale ?? false + let value = result.data + + queue.sync { + trace.append("resulted (\(stale)): \(op.id) \(value) (\(op.kind.rawValue))") + } + } + .store(in: &self.cancellables) + + fn(operations, results) + + operations.send(completion: .finished) + results.send(completion: .finished) } - .store(in: &self.cancellables) - - fn(operations, results) - - return trace } - + /// Mock operation that we use in the tests. private static let queryOperation = SwiftGraphQLClient.Operation( id: "qur-id", @@ -69,7 +87,7 @@ final class CacheExchangeTests: XCTestCase { types: ["A", "B", "C"], args: ExecutionArgs(query: "", variables: [:]) ) - /// Mock operation that we use in the tests. + /// Mock operation that we use in the tests. private static let mutationOperation = SwiftGraphQLClient.Operation( id: "mut-id", kind: .mutation, @@ -78,8 +96,8 @@ final class CacheExchangeTests: XCTestCase { types: ["A", "B"], args: ExecutionArgs(query: "", variables: [:]) ) - - /// Mock operation that we use in the tests. + + /// Mock operation that we use in the tests. private static let subscriptionOperation = SwiftGraphQLClient.Operation( id: "sub-id", kind: .subscription, @@ -88,198 +106,221 @@ final class CacheExchangeTests: XCTestCase { types: ["A", "B"], args: ExecutionArgs(query: "", variables: [:]) ) - + // MARK: - On Query - - func testOnQueryCacheFirstHitDoesNotForwardRequest() throws { - let trace = environment { operations, results in + + func testOnQueryCacheFirstHitDoesNotForwardRequest() async throws { + let trace = await environment { operations, results in operations.send(CacheExchangeTests.queryOperation.with(policy: .cacheFirst)) operations.send(CacheExchangeTests.queryOperation.with(policy: .cacheFirst)) } - - XCTAssertEqual(trace, [ - "requested: qur-id (query, cache-first)", - "forwarded: qur-id (query, cache-first)", - "requested: qur-id (query, cache-first)", - "forwarded: qur-id (query, cache-first)", - ]) + + XCTAssertEqual( + trace, + [ + "requested: qur-id (query, cache-first)", + "forwarded: qur-id (query, cache-first)", + "requested: qur-id (query, cache-first)", + "forwarded: qur-id (query, cache-first)", + ]) } - - func testOnQueryCacheFirstForwardsMissingRequest() throws { - let trace = environment { operations, results in + + func testOnQueryCacheFirstForwardsMissingRequest() async throws { + let trace = await environment { operations, results in let op = CacheExchangeTests.queryOperation.with(policy: .cacheFirst) - + operations.send(op) - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("hello"), - error: nil, - stale: false - )) + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("hello"), + error: nil, + stale: false + )) operations.send(op) } - - XCTAssertEqual(trace, [ - "requested: qur-id (query, cache-first)", - "forwarded: qur-id (query, cache-first)", - "resulted (false): qur-id hello (query)", - "requested: qur-id (query, cache-first)", - "resulted (false): qur-id hello (query)", - ]) + + XCTAssertEqual( + trace, + [ + "requested: qur-id (query, cache-first)", + "forwarded: qur-id (query, cache-first)", + "resulted (false): qur-id hello (query)", + "requested: qur-id (query, cache-first)", + "resulted (false): qur-id hello (query)", + ]) } - - func testCacheAndNetworkForwardsRequest() throws { - let trace = environment { operations, results in + + func testCacheAndNetworkForwardsRequest() async throws { + let trace = await environment { operations, results in let op = CacheExchangeTests.queryOperation.with(policy: .cacheAndNetwork) - + operations.send(op) - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("hello"), - error: nil, - stale: false - )) + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("hello"), + error: nil, + stale: false + )) operations.send(op) - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("world"), - error: nil, - stale: false - )) + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("world"), + error: nil, + stale: false + )) } - + // Forwarded operation and cached result may come in arbitrary order but both synchronously. - XCTAssertEqual(trace[0..<4], [ - "requested: qur-id (query, cache-and-network)", - "forwarded: qur-id (query, cache-and-network)", - "resulted (false): qur-id hello (query)", - "requested: qur-id (query, cache-and-network)", - ]) + XCTAssertEqual( + trace[0..<4], + [ + "requested: qur-id (query, cache-and-network)", + "forwarded: qur-id (query, cache-and-network)", + "resulted (false): qur-id hello (query)", + "requested: qur-id (query, cache-and-network)", + ]) XCTAssertEqual( Set(trace[4..<6]), Set([ "resulted (true): qur-id hello (query)", - "forwarded: qur-id (query, cache-and-network)" + "forwarded: qur-id (query, cache-and-network)", ]) ) XCTAssertEqual(trace[6], "resulted (false): qur-id world (query)") } - - func testCacheOnlyDoesntForwardRequest() throws { - let trace = environment { operations, results in + + func testCacheOnlyDoesntForwardRequest() async throws { + let trace = await environment { operations, results in let op = CacheExchangeTests.queryOperation.with(policy: .cacheOnly) - + operations.send(op) - + // randomly, unexplicably receive the result - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("hello"), - error: nil, - stale: false - )) - + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("hello"), + error: nil, + stale: false + )) + operations.send(op) } - + // Forwarded operation and cached result may come in arbitrary order but both synchronously. - XCTAssertEqual(trace, [ - "requested: qur-id (query, cache-only)", - "resulted (false): qur-id hello (query)", // manually triggered - "requested: qur-id (query, cache-only)", - "resulted (false): qur-id hello (query)", // cache-only is never stale - ]) + XCTAssertEqual( + trace, + [ + "requested: qur-id (query, cache-only)", + "resulted (false): qur-id hello (query)", // manually triggered + "requested: qur-id (query, cache-only)", + "resulted (false): qur-id hello (query)", // cache-only is never stale + ]) } - - func testNetworkOnlyForwardRequest() throws { - let trace = environment { operations, results in + + func testNetworkOnlyForwardRequest() async throws { + let trace = await environment { operations, results in let op = CacheExchangeTests.queryOperation.with(policy: .networkOnly) - + operations.send(op) - + // randomly, unexplicably receive the result - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("hello"), - error: nil, - stale: false - )) - + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("hello"), + error: nil, + stale: false + )) + operations.send(op) } - + // Forwarded operation and cached result may come in arbitrary order but both synchronously. - XCTAssertEqual(trace, [ - "requested: qur-id (query, network-only)", - "forwarded: qur-id (query, network-only)", - "resulted (false): qur-id hello (query)", - "requested: qur-id (query, network-only)", - "forwarded: qur-id (query, network-only)", - ]) + XCTAssertEqual( + trace, + [ + "requested: qur-id (query, network-only)", + "forwarded: qur-id (query, network-only)", + "resulted (false): qur-id hello (query)", + "requested: qur-id (query, network-only)", + "forwarded: qur-id (query, network-only)", + ]) } - + // MARK: - On Mutation - - func testOnMutationDoesNotCache() throws { - let trace = environment { operations, results in + + func testOnMutationDoesNotCache() async throws { + let trace = await environment { operations, results in operations.send(CacheExchangeTests.mutationOperation) operations.send(CacheExchangeTests.mutationOperation) } - - XCTAssertEqual(trace, [ - "requested: mut-id (mutation, cache-and-network)", - "forwarded: mut-id (mutation, cache-and-network)", - "requested: mut-id (mutation, cache-and-network)", - "forwarded: mut-id (mutation, cache-and-network)", - ]) + + XCTAssertEqual( + trace, + [ + "requested: mut-id (mutation, cache-and-network)", + "forwarded: mut-id (mutation, cache-and-network)", + "requested: mut-id (mutation, cache-and-network)", + "forwarded: mut-id (mutation, cache-and-network)", + ]) } - - func testMutationInvalidatesQueries() throws { - let trace = environment { operations, results in + + func testMutationInvalidatesQueries() async throws { + let trace = await environment { operations, results in let op = CacheExchangeTests.queryOperation.with(policy: .cacheAndNetwork) - + operations.send(op) - - results.send(SwiftGraphQLClient.OperationResult( - operation: op, - data: AnyCodable("hello"), - error: nil, - stale: false - )) - + + results.send( + SwiftGraphQLClient.OperationResult( + operation: op, + data: AnyCodable("hello"), + error: nil, + stale: false + )) + // somehow receive mutation result - results.send(SwiftGraphQLClient.OperationResult( - operation: CacheExchangeTests.mutationOperation, - data: AnyCodable("much data"), - error: nil, - stale: false - )) + results.send( + SwiftGraphQLClient.OperationResult( + operation: CacheExchangeTests.mutationOperation, + data: AnyCodable("much data"), + error: nil, + stale: false + )) } - + // Forwarded operation and cached result may come in arbitrary order but both synchronously. - XCTAssertEqual(trace, [ - "requested: qur-id (query, cache-and-network)", - "forwarded: qur-id (query, cache-and-network)", - "resulted (false): qur-id hello (query)", - "reexecuted: qur-id (query, network-only)", - "resulted (false): mut-id much data (mutation)", - // reexecute of mock client doesn't re-enter operation into the pipeline - ]) + XCTAssertEqual( + trace, + [ + "requested: qur-id (query, cache-and-network)", + "forwarded: qur-id (query, cache-and-network)", + "resulted (false): qur-id hello (query)", + "reexecuted: qur-id (query, network-only)", + "resulted (false): mut-id much data (mutation)", + // reexecute of mock client doesn't re-enter operation into the pipeline + ]) } - + // MARK: - On Subscription - - func testOnSubscriptionForwardsSubscription() throws { - let trace = environment { operations, results in + + func testOnSubscriptionForwardsSubscription() async throws { + let trace = await environment { operations, results in operations.send(CacheExchangeTests.subscriptionOperation) operations.send(CacheExchangeTests.subscriptionOperation) } - - XCTAssertEqual(trace, [ - "requested: sub-id (subscription, cache-and-network)", - "forwarded: sub-id (subscription, cache-and-network)", - "requested: sub-id (subscription, cache-and-network)", - "forwarded: sub-id (subscription, cache-and-network)", - ]) + + XCTAssertEqual( + trace, + [ + "requested: sub-id (subscription, cache-and-network)", + "forwarded: sub-id (subscription, cache-and-network)", + "requested: sub-id (subscription, cache-and-network)", + "forwarded: sub-id (subscription, cache-and-network)", + ]) } } diff --git a/Tests/SwiftGraphQLClientTests/Exchanges/ComposeExchangeTests.swift b/Tests/SwiftGraphQLClientTests/Exchanges/ComposeExchangeTests.swift index c4f5aef5..8fca2a3d 100644 --- a/Tests/SwiftGraphQLClientTests/Exchanges/ComposeExchangeTests.swift +++ b/Tests/SwiftGraphQLClientTests/Exchanges/ComposeExchangeTests.swift @@ -3,7 +3,7 @@ import GraphQL @testable import SwiftGraphQLClient import XCTest -final class ComposeExchangeTests: XCTestCase { +@MainActor final class ComposeExchangeTests: XCTestCase { private struct DebugExchange: Exchange { diff --git a/Tests/SwiftGraphQLClientTests/Exchanges/FallbackExchangeTests.swift b/Tests/SwiftGraphQLClientTests/Exchanges/FallbackExchangeTests.swift index 262ef03b..242cf8f7 100644 --- a/Tests/SwiftGraphQLClientTests/Exchanges/FallbackExchangeTests.swift +++ b/Tests/SwiftGraphQLClientTests/Exchanges/FallbackExchangeTests.swift @@ -3,7 +3,7 @@ import GraphQL @testable import SwiftGraphQLClient import XCTest -final class FallbackExchangeTests: XCTestCase { +@MainActor final class FallbackExchangeTests: XCTestCase { private var cancellables = Set() diff --git a/Tests/SwiftGraphQLClientTests/Exchanges/FetchExchangeTests.swift b/Tests/SwiftGraphQLClientTests/Exchanges/FetchExchangeTests.swift index e9b72123..ccde63b3 100644 --- a/Tests/SwiftGraphQLClientTests/Exchanges/FetchExchangeTests.swift +++ b/Tests/SwiftGraphQLClientTests/Exchanges/FetchExchangeTests.swift @@ -3,7 +3,7 @@ import GraphQL @testable import SwiftGraphQLClient import XCTest -final class FetchExchangeTests: XCTestCase { +@MainActor final class FetchExchangeTests: XCTestCase { struct MockURLSession: FetchSession { /// Mock handler used to create a mock data response of the request. diff --git a/Tests/SwiftGraphQLClientTests/Extensions/Publishers+ExtensionsTests.swift b/Tests/SwiftGraphQLClientTests/Extensions/Publishers+ExtensionsTests.swift index 472eed3d..bdbceb35 100644 --- a/Tests/SwiftGraphQLClientTests/Extensions/Publishers+ExtensionsTests.swift +++ b/Tests/SwiftGraphQLClientTests/Extensions/Publishers+ExtensionsTests.swift @@ -2,7 +2,7 @@ import Combine @testable import SwiftGraphQLClient import XCTest -final class PublishersExtensionsTests: XCTestCase { +@MainActor final class PublishersExtensionsTests: XCTestCase { var cancellables = Set() @@ -129,7 +129,7 @@ final class PublishersExtensionsTests: XCTestCase { XCTAssertEqual(value, 1) } - func testThrowEmittedErrorAsynchronously() async throws { + nonisolated func testThrowEmittedErrorAsynchronously() async throws { struct TestError: Error {} await XCTAssertThrowsError(of: TestError.self) { diff --git a/Tests/SwiftGraphQLClientTests/Utils/MockClient.swift b/Tests/SwiftGraphQLClientTests/Utils/MockClient.swift index 9782aacf..2ce36ad0 100644 --- a/Tests/SwiftGraphQLClientTests/Utils/MockClient.swift +++ b/Tests/SwiftGraphQLClientTests/Utils/MockClient.swift @@ -4,7 +4,7 @@ import Logging import SwiftGraphQLClient /// A client that you can use to perform tests on exchanges. -class MockClient: GraphQLClient { +class MockClient: GraphQLClient, @unchecked Sendable { var request: URLRequest private var customExecute: ((SwiftGraphQLClient.Operation) -> AnyPublisher)? diff --git a/Tests/SwiftGraphQLCodegenTests/Generator/InputObjectTests.swift b/Tests/SwiftGraphQLCodegenTests/Generator/InputObjectTests.swift index 3c54b1dc..2a2da79e 100644 --- a/Tests/SwiftGraphQLCodegenTests/Generator/InputObjectTests.swift +++ b/Tests/SwiftGraphQLCodegenTests/Generator/InputObjectTests.swift @@ -30,7 +30,7 @@ final class InputObjectTests: XCTestCase { generated.assertInlineSnapshot(matching: """ extension InputObjects { - public struct InputObject: Encodable, Hashable { + public struct InputObject: Encodable, Hashable, Sendable { /// Field description. /// Multiline. @@ -81,7 +81,7 @@ final class InputObjectTests: XCTestCase { generated.assertInlineSnapshot(matching: """ extension InputObjects { - public struct InputObject: Encodable, Hashable { + public struct InputObject: Encodable, Hashable, Sendable { /// Field description. public var id: Enums.Enum diff --git a/Tests/SwiftGraphQLCodegenTests/Integration/API.swift b/Tests/SwiftGraphQLCodegenTests/Integration/API.swift index c1b1b10b..0233e253 100644 --- a/Tests/SwiftGraphQLCodegenTests/Integration/API.swift +++ b/Tests/SwiftGraphQLCodegenTests/Integration/API.swift @@ -1302,6 +1302,10 @@ extension Fields where TypeLock == Unions.SearchResult { } } +extension Never: Sendable { + +} + extension Selection where TypeLock == Never, T == Never { typealias SearchResult = Selection } @@ -1356,7 +1360,7 @@ extension Selection where TypeLock == Never, T == Never { enum Enums {} extension Enums { /// Item - enum Item: String, CaseIterable, Codable { + enum Item: String, CaseIterable, Codable, Sendable { case character = "CHARACTER" @@ -1381,7 +1385,7 @@ extension Enums.Item: GraphQLScalar { } } - static var mockValue = Self.character + static let mockValue = Self.character } // MARK: - Input Objects @@ -1391,7 +1395,7 @@ typealias Inputs = InputObjects enum InputObjects {} extension InputObjects { - struct Pagination: Encodable, Hashable { + struct Pagination: Encodable, Hashable, Sendable { var offset: OptionalArgument = .init() /// Number of items in a list that should be returned. @@ -1411,7 +1415,7 @@ extension InputObjects { } } extension InputObjects { - struct Search: Encodable, Hashable { + struct Search: Encodable, Hashable, Sendable { /// String used to compare the name of the item to. var query: String diff --git a/Tests/SwiftGraphQLTests/Integration/HTTPTests.swift b/Tests/SwiftGraphQLTests/Integration/HTTPTests.swift index 89d1841b..0cb66ee4 100644 --- a/Tests/SwiftGraphQLTests/Integration/HTTPTests.swift +++ b/Tests/SwiftGraphQLTests/Integration/HTTPTests.swift @@ -2,7 +2,7 @@ import XCTest /// Tests the serialization of the query from the AST. -final class HTTPTests: XCTestCase { +@MainActor final class HTTPTests: XCTestCase { /// Tests basic HTTP query performed against a server. func testHTTPQuery() throws { diff --git a/Tests/SwiftGraphQLTests/Serialization/OptionalArgumentTest.swift b/Tests/SwiftGraphQLTests/Serialization/OptionalArgumentTest.swift index 77a8ffb6..49e522e7 100644 --- a/Tests/SwiftGraphQLTests/Serialization/OptionalArgumentTest.swift +++ b/Tests/SwiftGraphQLTests/Serialization/OptionalArgumentTest.swift @@ -1,11 +1,11 @@ @testable import SwiftGraphQL import XCTest -final class OptionalArgumentTests: XCTestCase { +@MainActor final class OptionalArgumentTests: XCTestCase { // MARK: - Recursive types func testRecursiveOptionalType() { - struct Person { + struct Person: Sendable { var name: String var friends: OptionalArgument } diff --git a/archive/swift-graphql-5.1.3.tar.gz b/archive/swift-graphql-5.1.3.tar.gz new file mode 100644 index 00000000..3e7b7d07 Binary files /dev/null and b/archive/swift-graphql-5.1.3.tar.gz differ