diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index abc9b6a..7c89a91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - focal - bionic tag: - - swift:5.4 + - swift:5.5 container: image: ${{ matrix.tag }}-${{ matrix.os }} steps: diff --git a/.gitignore b/.gitignore index 10a7cb7..03bef73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ .DS_Store ### SwiftPM ### -/.build -/.swiftpm +.build/ +.swiftpm/ + +### Jetbrains ### +.idea/ + diff --git a/Package.resolved b/Package.resolved index dc27847..b7ba3d2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,22 +1,13 @@ { "object": { "pins": [ - { - "package": "GraphQL", - "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", - "state": { - "branch": null, - "revision": "e5de315125f8220334ba3799bbd78c7c1ed529f7", - "version": "2.0.0" - } - }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections", "state": { "branch": null, - "revision": "d45e63421d3dff834949ac69d3c37691e994bd69", - "version": "0.0.3" + "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", + "version": "1.0.1" } }, { @@ -24,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "d161bf658780b209c185994528e7e24376cf7283", - "version": "2.29.0" + "revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef", + "version": "2.33.0" } } ] diff --git a/Package.swift b/Package.swift index f273026..0d1a48b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.3 import PackageDescription let package = Package( @@ -7,7 +7,9 @@ let package = Package( .library(name: "Graphiti", targets: ["Graphiti"]), ], dependencies: [ - .package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "2.0.0")) +// .package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "2.0.0")), +// .package(url: "https://github.com/GraphQLSwift/GraphQL.git", .branch("feature/async-await")), + .package(path: "../GraphQL") ], targets: [ .target(name: "Graphiti", dependencies: ["GraphQL"]), diff --git a/README.md b/README.md index a87e01b..9972aa9 100644 --- a/README.md +++ b/README.md @@ -23,135 +23,403 @@ through that README and the corresponding tests in parallel. ### Using Graphiti -Add Graphiti to your `Package.swift` +Add Graphiti to your `Package.swift`. Graphiti provides two important capabilities: building a type schema, and +serving queries against that type schema. -```swift -import PackageDescription +### Defining entities -let package = Package( - dependencies: [ - .Package(url: "https://github.com/GraphQLSwift/Graphiti.git", .upToNextMinor(from: "0.20.1")), - ] -) +First, we create our regular Swift entities. For our example, we are using the quintessential counter. The only requirements are that GraphQL output types must conform to `Encodable` and GraphQL input types must conform to `Decodable`. + +```swift +struct Count: Encodable { + var value: Int +} ``` -Graphiti provides two important capabilities: building a type schema, and -serving queries against that type schema. +⭐️ Notice that this step does not require importing `Graphiti`. One of the main design decisions behind Graphiti is to **not** pollute your entities declarations. This way you can bring your entities to any other environment with ease. -#### Defining entities +#### Defining the business logic -First, we declare our regular Swift entities. +Then, we create the business logic of our API. The best suited type for this is an actor. Within this actor we define our state and all the different ways this state can be accessed and updated. This is the place where you put code that derives your entities from a database or any other service. You have complete design freedom here. ```swift -struct Message : Codable { - let content: String +actor CounterState { + var count: Count + + init(count: Count) { + self.count = count + } + + func increment() -> Count { + count.value += 1 + return count + } + + func decrement() -> Count { + count.value -= 1 + return count + } + + func increment(by amount: Int) -> Count { + count.value += amount + return count + } + + func decrement(by amount: Int) -> Count { + count.value -= amount + return count + } } ``` -⭐️ One of the main design decisions behind Graphiti is **not** to polute your entities declarations. This way you can bring your entities to any other solution with ease. - #### Defining the context -Second step is to create your application's **context**. The context will be passed to all of your field resolver functions. This allows you to apply dependency injection to your API. This is the place where you can put code that talks to a database or another service. +Third step is to create the GraphQL API context. The context will be passed to all of your field resolver functions. This allows you to apply dependency injection to your API. The context's role is to give the GraphQL resolvers access to your APIs business logic. ```swift -struct Context { - func message() -> Message { - Message(content: "Hello, world!") - } +struct CounterContext { + var count: () async -> Count + var increment: () async -> Count + var decrement: () async -> Count + var incrementBy: (_ amount: Int) async -> Count + var decrementBy: (_ amount: Int) async -> Count } ``` -⭐️ Notice again that this step doesn't require Graphiti. It's purely business logic. +You can model the context however you like. You could bypass the creation of a separate type and use your APIs actor directly as the GraphQL context. However, we do not encourage this, since it makes your API less testable. You could, for example, use a delegate protocol that would allow you to have different implementations in different environments. Nonetheless, we prefer structs with mutable closure properties, because we can easily create different versions of a context by swapping specific closures, instead of having to create a complete type conforming to a delegate protocol every time we need a new behavior. With this design we can easily create a mocked version of our context when testing, for example. #### Defining the GraphQL API resolver -Now that we have our entities and context we can create the GraphQL API resolver. +Now we can create the GraphQL API root resolver. These root resolver functions will be used to resolve the queries and mutations defined in the schema. ```swift -import Graphiti - -struct Resolver { - func message(context: Context, arguments: NoArguments) -> Message { - context.message() +struct CounterResolver { + var count: (CounterContext, Void) async throws -> Count + var increment: (CounterContext, Void) async throws -> Count + var decrement: (CounterContext, Void) async throws -> Count + + struct IncrementByArguments: Decodable { + let amount: Int } + + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Count + + struct DecrementByArguments: Decodable { + let amount: Int + } + + var decrementBy: (CounterContext, DecrementByArguments) async throws -> Count } ``` +⭐️ Notice that this step does not require importing `Graphiti`. However, all resolver functions must take the following shape: + +```swift +(Context, Arguments) async throws -> Output where Arguments: Decodable +``` + +In case your resolve function does not use any arguments you can use the following shape: + + +```swift +(Context, Void) async throws -> Output +``` + +Our `CounterResolver` looks very similar to our `CounterContext`. First thing we notice is that we're using a struct with mutable closure properties again. We do this for the same reason we do it for `CounterContext`. To allow us to easily swap implementations in different environments. The closures themselves are also almost identical. The difference is that resolver functions need to follow the specific shapes we mentioned above. We do it this way because `Graphiti` needs a predictable structure to be able to decode arguments and execute the resolver function. Most of the time, the resolver function's role is to extract the parameters and forward the business logic to the context. + +Notice too that in this example there's a one-to-one mapping of the context's properties and the resolver's properties. This only happens for small applications. In a complex application, the root resolver might map to only a subset of the context's properties, because the context might contain additional logic that could be accessed by other resolver functions defined in custom GraphQL types, for example. + #### Defining the GraphQL API schema -Now we can finally define the GraphQL API with its schema. +At last, we define the GraphQL API with its schema. ```swift -struct MessageAPI : API { - let resolver: Resolver - let schema: Schema - - init(resolver: Resolver) throws { - self.resolver = resolver +import Graphiti - self.schema = try Schema { - Type(Message.self) { - Field("content", at: \.content) - } +struct CounterAPI { + let schema = Schema { + Type(Count.self) { + Field("value", at: \.value) + } + + Query { + Field("count", at: \.count) + } - Query { - Field("message", at: Resolver.message) + Mutation { + Field("increment", at: \.increment) + Field("decrement", at: \.decrement) + + Field("incrementBy", at: \.incrementBy) { + Argument("amount", at: \.amount) + } + + Field("decrementBy", at: \.decrementBy) { + Argument("amount", at: \.amount) } } } } ``` -⭐️ Notice that `API` allows dependency injection. You could pass mocks of `resolver` and `context` when testing, for example. +⭐️ Now we finally need to import Graphiti 😄. To check the equivalent GraphQL SDL use: + +```swift +let api = CounterAPI() +debugPrint(api.schema) +``` + +The output will be: + +```graphql +type Count { + value: Int! +} + +type Query { + count: Count! +} + +type Mutation { + increment: Count! + decrement: Count! + incrementBy(amount: Int!): Count! + decrementBy(amount: Int!): Count! +} +``` #### Querying -To query the schema we need to instantiate the api and pass in an EventLoopGroup to feed the execute function alongside the query itself. +To query the schema, we first need to create a live instance of the context: + +```swift +extension CounterContext { + static let live: CounterContext = { + let count = Count(value: 0) + let state = CounterState(count: count) + + return CounterContext( + count: { + await state.count + }, + increment: { + await state.increment() + }, + decrement: { + await state.decrement() + }, + incrementBy: { count in + await state.increment(by: count) + }, + decrementBy: { count in + await state.decrement(by: count) + } + ) + }() +} +``` + +Now we create a live instance of the resolver: + +```swift +extension CounterResolver { + static let live = CounterResolver( + count: { context, _ in + await context.count() + }, + increment: { context, _ in + await context.increment() + }, + decrement: { context, _ in + await context.decrement() + }, + incrementBy: { context, arguments in + await context.incrementBy(arguments.amount) + }, + decrementBy: { context, arguments in + await context.decrementBy(arguments.amount) + } + ) +} +``` + +This implementation basically extracts the arguments from the GraphQL query and delegates the business logic to the `context`. As mentioned before, you could create a `test` version of the context and the resolver when testing. Now we just need an `EventLoopGroup` from `NIO` and we're ready to query the API. ```swift import NIO -let resolver = Resolver() -let context = Context() -let api = try MessageAPI(resolver: resolver) let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) defer { try? group.syncShutdownGracefully() } -api.execute( - request: "{ message { content } }", - context: context, +let api = CounterAPI() + +let countQuery = """ +query { + count { + value + } +} +""" + +let countResult = try await api.schema.execute( + request: countQuery, + resolver: .live, + context: .live, on: group -).whenSuccess { result in - print(result) +) + +debugPrint(countResult) +``` + +The output will be: + +```json +{ + "data" : { + "count" : { + "value" : 0 + } + } +} +``` + +For the increment mutation: + +```swift +let incrementMutation = """ +mutation { + increment { + value + } +} +""" + +let incrementResult = try await api.schema.execute( + request: incrementMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(incrementResult) +``` + +The output will be: + +```json +{ + "data" : { + "increment" : { + "value" : 1 + } + } +} +``` + +For the decrement mutation: + +```swift +let decrementMutation = """ +mutation { + decrement { + value + } } +""" + +let decrementResult = try await api.schema.execute( + request: decrementMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(decrementResult) ``` The output will be: ```json -{"data":{"message":{"content":"Hello, world!"}}} +{ + "data" : { + "decrement" : { + "value" : 0 + } + } +} ``` -`API.execute` returns a `GraphQLResult` which adopts `Encodable`. You can use it with a `JSONEncoder` to send the response back to the client using JSON. +For the incrementBy mutation: -#### Async resolvers +```swift +let incrementByMutation = """ +mutation { + incrementBy(count: 5) { + value + } +} +""" -To use async resolvers, just add one more parameter with type `EventLoopGroup` to the resolver function and change the return type to `EventLoopFuture`. Don't forget to import NIO. +let incrementByResult = try await api.schema.execute( + request: incrementByMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(incrementByResult) +``` + +The output will be: + +```json +{ + "data" : { + "incrementBy" : { + "value" : 5 + } + } +} +``` + +For the decrementBy mutation: ```swift -import NIO +let decrementByMutation = """ +mutation { + decrementBy(count: 5) { + value + } +} +""" + +let decrementByResult = try await api.schema.execute( + request: decrementByMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(decrementByResult) +``` + +The output will be: -struct Resolver { - func message(context: Context, arguments: NoArguments, group: EventLoopGroup) -> EventLoopFuture { - group.next().makeSucceededFuture(context.message()) +```json +{ + "data" : { + "decrementBy" : { + "value" : 0 } + } } ``` +⭐️ `Schema.execute` returns a `GraphQLResult` which adopts `Encodable`. You can use it with a `JSONEncoder` to send the response back to the client using JSON. + #### Subscription This library supports GraphQL subscriptions. To use them, you must create a concrete subclass of the `EventStream` class that implements event streaming @@ -187,3 +455,4 @@ This project is released under the MIT license. See [LICENSE](LICENSE) for detai [coverage-badge]: https://api.codeclimate.com/v1/badges/25559824033fc2caa94e/test_coverage [coverage-url]: https://codeclimate.com/github/GraphQLSwift/Graphiti/test_coverage + diff --git a/Sources/Graphiti/API/API.swift b/Sources/Graphiti/API/API.swift index 9264b83..3d9b9a6 100644 --- a/Sources/Graphiti/API/API.swift +++ b/Sources/Graphiti/API/API.swift @@ -1,6 +1,7 @@ import GraphQL import NIO +@available(*, deprecated, message: "Use the schema directly.") public protocol API { associatedtype Resolver associatedtype ContextType @@ -16,11 +17,11 @@ extension API { variables: [String: Map] = [:], operationName: String? = nil ) -> EventLoopFuture { - return schema.execute( + schema.execute( request: request, resolver: resolver, context: context, - eventLoopGroup: eventLoopGroup, + on: eventLoopGroup, variables: variables, operationName: operationName ) @@ -33,11 +34,11 @@ extension API { variables: [String: Map] = [:], operationName: String? = nil ) -> EventLoopFuture { - return schema.subscribe( + schema.subscribe( request: request, resolver: resolver, context: context, - eventLoopGroup: eventLoopGroup, + on: eventLoopGroup, variables: variables, operationName: operationName ) diff --git a/Sources/Graphiti/Argument/Argument.swift b/Sources/Graphiti/Argument/Argument.swift index f8d5c89..45af548 100644 --- a/Sources/Graphiti/Argument/Argument.swift +++ b/Sources/Graphiti/Argument/Argument.swift @@ -16,12 +16,26 @@ public class Argument : ArgumentCompone init(name: String) { self.name = name + super.init() + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") } } public extension Argument { + @available(*, deprecated, message: "Use Argument.init(_:of:at:) instead.") + convenience init( + _ name: String, + at keyPath: KeyPath + ) { + self.init(name:name) + } + convenience init( _ name: String, + of type: ArgumentType.Type, at keyPath: KeyPath ) { self.init(name:name) diff --git a/Sources/Graphiti/Argument/ArgumentComponent.swift b/Sources/Graphiti/Argument/ArgumentComponent.swift index 87ed3ee..21c039e 100644 --- a/Sources/Graphiti/Argument/ArgumentComponent.swift +++ b/Sources/Graphiti/Argument/ArgumentComponent.swift @@ -1,14 +1,21 @@ import GraphQL -public class ArgumentComponent { +public class ArgumentComponent: ExpressibleByStringLiteral { var description: String? = nil + init() {} + func argument(typeProvider: TypeProvider, coders: Coders) throws -> (String, GraphQLArgument) { fatalError() } + + public required init(stringLiteral string: StringLiteralType) { + self.description = string + } } public extension ArgumentComponent { + @available(*, deprecated, message: "Use a string literal above a component to give it a description.") func description(_ description: String) -> Self { self.description = description return self diff --git a/Sources/Graphiti/Argument/NoArguments.swift b/Sources/Graphiti/Argument/NoArguments.swift index 05cc88e..1d79dd7 100644 --- a/Sources/Graphiti/Argument/NoArguments.swift +++ b/Sources/Graphiti/Argument/NoArguments.swift @@ -1,3 +1,4 @@ -public struct NoArguments : Decodable { +@available(*, deprecated, message: "Use the Field initializer with the resolve function that takes a Void as an argument.") +public struct NoArguments: Decodable { init() {} } diff --git a/Sources/Graphiti/Component/Component.swift b/Sources/Graphiti/Component/Component.swift index 298d515..e2bfdff 100644 --- a/Sources/Graphiti/Component/Component.swift +++ b/Sources/Graphiti/Component/Component.swift @@ -1,6 +1,6 @@ import GraphQL -open class Component { +open class Component: ExpressibleByStringLiteral { let name: String var description: String? = nil @@ -8,11 +8,16 @@ open class Component { self.name = name } + public required init(stringLiteral string: StringLiteralType) { + self.name = "" + self.description = string + } func update(typeProvider: SchemaTypeProvider, coders: Coders) throws {} } public extension Component { + @available(*, deprecated, message: "Use a string literal above a component to give it a description.") func description(_ description: String) -> Self { self.description = description return self diff --git a/Sources/Graphiti/Connection/Connection.swift b/Sources/Graphiti/Connection/Connection.swift index a9c889e..16b862b 100644 --- a/Sources/Graphiti/Connection/Connection.swift +++ b/Sources/Graphiti/Connection/Connection.swift @@ -2,13 +2,13 @@ import Foundation import NIO import GraphQL -public struct Connection : Encodable { +public struct Connection: Encodable { let edges: [Edge] let pageInfo: PageInfo } @available(OSX 10.15, *) -public extension Connection where Node : Identifiable, Node.ID : LosslessStringConvertible { +public extension Connection where Node: Identifiable, Node.ID: LosslessStringConvertible { static func id(_ cursor: String) -> Node.ID? { cursor.base64Decoded().flatMap({ Node.ID($0) }) } @@ -19,7 +19,7 @@ public extension Connection where Node : Identifiable, Node.ID : LosslessStringC } @available(OSX 10.15, *) -public extension EventLoopFuture where Value : Sequence, Value.Element : Encodable & Identifiable, Value.Element.ID : LosslessStringConvertible { +public extension EventLoopFuture where Value: Sequence, Value.Element: Encodable & Identifiable, Value.Element.ID: LosslessStringConvertible { func connection(from arguments: Paginatable) -> EventLoopFuture> { connection(from: arguments, makeCursor: Connection.cursor) } diff --git a/Sources/Graphiti/Connection/ConnectionType.swift b/Sources/Graphiti/Connection/ConnectionType.swift index a1ffb9e..60057eb 100644 --- a/Sources/Graphiti/Connection/ConnectionType.swift +++ b/Sources/Graphiti/Connection/ConnectionType.swift @@ -31,6 +31,18 @@ public final class ConnectionType : C private init(type: ObjectType.Type) { super.init(name: "") } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } } public extension ConnectionType { diff --git a/Sources/Graphiti/Context/NoContext.swift b/Sources/Graphiti/Context/NoContext.swift index f5d8b04..960a940 100644 --- a/Sources/Graphiti/Context/NoContext.swift +++ b/Sources/Graphiti/Context/NoContext.swift @@ -1 +1,2 @@ +@available(*, deprecated, message: "Use Void directly.") public typealias NoContext = Void diff --git a/Sources/Graphiti/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift new file mode 100644 index 0000000..2c6f211 --- /dev/null +++ b/Sources/Graphiti/Directive/Directive.swift @@ -0,0 +1,107 @@ +import GraphQL + +#warning("TODO: Allow custom metadata to the types? How could this custom metadata change behavior? Expose an additional parameter in the resolve functions?") +public final class Directive: Component where DirectiveType: Decodable { + private let locations: [GraphQL.DirectiveLocation] + private let arguments: [ArgumentComponent] + + override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { + #warning("TODO: Make description optional in GraphQLDirective") +// guard let description = self.description else { +// throw GraphQLError(message: "No description. Descriptions are required for directives") +// } + + let directive = try GraphQLDirective( + name: name, + description: description ?? "", + locations: locations, + args: try arguments(typeProvider: typeProvider, coders: coders) + ) + + #warning("TODO: Guarantee there is no other directive with same name") + #warning("TODO: Map DirectiveType to GraphQLDirective") + typeProvider.directives.append((directive, decodeDirective)) + } + + func decodeDirective(map: Map, coders: Coders) throws -> Any { + try coders.decoder.decode(DirectiveType.self, from: map) + } + + func arguments(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLArgumentConfigMap { + var map: GraphQLArgumentConfigMap = [:] + + for argument in arguments { + let (name, argument) = try argument.argument(typeProvider: typeProvider, coders: coders) + map[name] = argument + } + + return map + } + + private init( + type: DirectiveType.Type, + name: String?, + locations: [GraphQL.DirectiveLocation], + arguments: [ArgumentComponent] + ) { + #warning("TODO: Throw if name equals pre-defined directives") + self.locations = locations + self.arguments = arguments + + super.init( + name: name ?? Reflection.name(for: DirectiveType.self).firstCharacterLowercased() + ) + } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } +} + +extension StringProtocol { + func firstCharacterLowercased() -> String { + prefix(1).lowercased() + dropFirst() + } +} + +public extension Directive { + convenience init( + _ type: DirectiveType.Type, + as name: String? = nil, + @ArgumentComponentBuilder argument: () -> ArgumentComponent, + @DirectiveLocationBuilder on locations: () -> [DirectiveLocation] + ) { + self.init( + type: type, + name: name, + locations: locations().map({ $0.location }), + arguments: [argument()] + ) + } + + convenience init( + _ type: DirectiveType.Type, + as name: String? = nil, + @ArgumentComponentBuilder arguments: () -> [ArgumentComponent] = {[]}, + @DirectiveLocationBuilder on locations: () -> [DirectiveLocation] + ) { + self.init( + type: type, + name: name, + locations: locations().map({ $0.location }), + arguments: arguments() + ) + } +} + + + + diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift new file mode 100644 index 0000000..675ce76 --- /dev/null +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift @@ -0,0 +1,100 @@ +import GraphQL + +public final class DirectiveLocation { + let location: GraphQL.DirectiveLocation + + init(location: GraphQL.DirectiveLocation) { + self.location = location + } +} + +// MARK: - Query + +// MARK: - Mutation + +// MARK: - Subscription + +// MARK: - Field + +public struct FieldDirectiveLocation { + public static let field = FieldDirectiveLocation() +} + +public protocol FieldDirective { + func field(resolve: @escaping AsyncResolve) -> AsyncResolve +} + +public extension DirectiveLocation { + convenience init(_ location: FieldDirectiveLocation) where DirectiveType: FieldDirective { + self.init(location: .field) + } +} + +// MARK: - FragmentDefinition + +// MARK: - FragmentSpread + +// MARK: - InlineFragment + +// MARK: - VariableDefinition + +// MARK: - Schema + +// MARK: - Scalar + +// MARK: - Object + +public struct ObjectDirectiveLocation { + public static let object = ObjectDirectiveLocation() +} + +public typealias Object = Type + +public protocol ObjectDirective { + func object(object: Object) where ObjectType: Encodable +} + +public extension DirectiveLocation { + convenience init(_ location: ObjectDirectiveLocation) where DirectiveType: ObjectDirective { + self.init(location: .object) + } +} + +// MARK: - FieldDefinition + +public struct FieldDefinitionDirectiveLocation { + public static let fieldDefinition = FieldDefinitionDirectiveLocation() +} + +public protocol FieldDefinitionDirective { + func fieldDefinition(field: Field) +} + +public extension DirectiveLocation { + convenience init(_ location: FieldDefinitionDirectiveLocation) where DirectiveType: FieldDefinitionDirective { + self.init(location: .fieldDefinition) + } +} + +// MARK: - ArgumentDefinition +// MARK: - Interface + +public struct InterfaceDirectiveLocation { + public static let interface = InterfaceDirectiveLocation() +} + +public protocol InterfaceDirective { + func interface(interface: Interface) +} + +public extension DirectiveLocation { + convenience init(_ location: InterfaceDirectiveLocation) where DirectiveType: InterfaceDirective { + self.init(location: .interface) + } +} + +// MARK: - Union +// MARK: - Enum +// MARK: - EnumValue +// MARK: - InputObject +// MARK: - InputFieldDefinition diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocationBuilder.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocationBuilder.swift new file mode 100644 index 0000000..c696c5d --- /dev/null +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocationBuilder.swift @@ -0,0 +1,11 @@ +@resultBuilder +public struct DirectiveLocationBuilder { + public static func buildExpression(_ value: DirectiveLocation) -> DirectiveLocation { + value + } + + public static func buildBlock(_ value: DirectiveLocation...) -> [DirectiveLocation] { + value + } +} + diff --git a/Sources/Graphiti/Enum/Enum.swift b/Sources/Graphiti/Enum/Enum.swift index a0f1e5b..a1e9a0d 100644 --- a/Sources/Graphiti/Enum/Enum.swift +++ b/Sources/Graphiti/Enum/Enum.swift @@ -1,39 +1,61 @@ import GraphQL -public final class Enum : Component where EnumType.RawValue == String { - private let values: [Value] +public final class Enum: Component where EnumType: Encodable & RawRepresentable, EnumType.RawValue == String { + private let enumValues: [EnumValueComponent] override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { let enumType = try GraphQLEnumType( name: name, description: description, - values: values.reduce(into: [:]) { result, value in - result[value.value.rawValue] = GraphQLEnumValue( - value: try MapEncoder().encode(value.value), - description: value.description, - deprecationReason: value.deprecationReason - ) - } + values: enumValueMap(typeProvider: typeProvider, coders: coders) ) try typeProvider.map(EnumType.self, to: enumType) } + func enumValueMap(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLEnumValueMap { + var map: GraphQLEnumValueMap = [:] + + for enumValue in enumValues { + let (name, enumValue) = try enumValue.enumValue(typeProvider: typeProvider, coders: coders) + map[name] = enumValue + } + + return map + } + private init( type: EnumType.Type, name: String?, - values: [Value] + values: [EnumValueComponent] ) { - self.values = values + var description: String? = nil + + self.enumValues = values.reduce([]) { result, component in + if let value = component as? Value { + value.description = description + description = nil + return result + [value] + } else if let componentDescription = component.description { + description = (description ?? "") + componentDescription + } + + return result + } + super.init(name: name ?? Reflection.name(for: EnumType.self)) } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } } public extension Enum { convenience init( _ type: EnumType.Type, as name: String? = nil, - @ValueBuilder _ values: () -> Value + @ValueBuilder _ values: () -> EnumValueComponent ) { self.init(type: type, name: name, values: [values()]) } @@ -41,7 +63,7 @@ public extension Enum { convenience init( _ type: EnumType.Type, as name: String? = nil, - @ValueBuilder _ values: () -> [Value] + @ValueBuilder _ values: () -> [EnumValueComponent] ) { self.init(type: type, name: name, values: values()) } diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 8745af0..57ef649 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -1,28 +1,55 @@ import GraphQL import NIO -public class Field : FieldComponent { +public class Field: FieldComponent where Arguments: Decodable { let name: String let arguments: [ArgumentComponent] - let resolve: AsyncResolve + var resolve: AsyncResolve + private var directives: [FieldDefinitionDirective] = [] - override func field(typeProvider: TypeProvider, coders: Coders) throws -> (String, GraphQLField) { + override func field(typeProvider: SchemaTypeProvider, coders: Coders) throws -> (String, GraphQLField) { + applyDirectives() + let field = GraphQLField( type: try typeProvider.getOutputType(from: FieldType.self, field: name), description: description, deprecationReason: deprecationReason, args: try arguments(typeProvider: typeProvider, coders: coders), - resolve: { source, arguments, context, eventLoopGroup, _ in - guard let s = source as? ObjectType else { + resolve: { source, arguments, context, eventLoopGroup, info in + var resolve = self.resolve + + for fieldDirective in info.fieldASTs[0].directives { + for (schemaDirective, decodeDirective) in typeProvider.directives { + if fieldDirective.name.value == schemaDirective.name { + print("Found directive \(schemaDirective.name)") + + let directiveMap = try getArgumentValues( + argDefs: schemaDirective.args, + argASTs: fieldDirective.arguments, + variables: info.variableValues + ) + + let directive = try decodeDirective(directiveMap, coders) + + guard let fieldDirective = directive as? FieldDirective else { + fatalError("Unreachable") + } + + resolve = fieldDirective.field(resolve: resolve) + } + } + } + + guard let source = source as? ObjectType else { throw GraphQLError(message: "Expected source type \(ObjectType.self) but got \(type(of: source))") } - guard let c = context as? Context else { + guard let context = context as? Context else { throw GraphQLError(message: "Expected context type \(Context.self) but got \(type(of: context))") } - let a = try coders.decoder.decode(Arguments.self, from: arguments) - return try self.resolve(s)(c, a, eventLoopGroup) + let arguments = try coders.decoder.decode(Arguments.self, from: arguments) + return try resolve(source)(context, arguments, eventLoopGroup) } ) @@ -40,6 +67,14 @@ public class Field : Fiel return map } + func applyDirectives() { + for directive in directives { + #warning("TODO: Check if directive exists schema") + #warning("TODO: Check for repeatable") + directive.fieldDefinition(field: self) + } + } + init( name: String, arguments: [ArgumentComponent], @@ -48,6 +83,7 @@ public class Field : Fiel self.name = name self.arguments = arguments self.resolve = resolve + super.init() } convenience init( @@ -60,6 +96,7 @@ public class Field : Fiel return try asyncResolve(type)(context, arguments, eventLoopGroup).map { $0 as Any? } } } + self.init(name: name, arguments: arguments, resolve: resolve) } @@ -93,11 +130,16 @@ public class Field : Fiel self.init(name: name, arguments: arguments, asyncResolve: asyncResolve) } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } } // MARK: AsyncResolve Initializers public extension Field where FieldType : Encodable { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping AsyncResolve, @@ -106,6 +148,7 @@ public extension Field where FieldType : Encodable { self.init(name: name, arguments: [argument()], asyncResolve: function) } + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping AsyncResolve, @@ -116,6 +159,7 @@ public extension Field where FieldType : Encodable { } public extension Field { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping AsyncResolve, @@ -125,6 +169,7 @@ public extension Field { self.init(name: name, arguments: [argument()], asyncResolve: function) } + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping AsyncResolve, @@ -138,6 +183,7 @@ public extension Field { // MARK: SimpleAsyncResolve Initializers public extension Field where FieldType : Encodable { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SimpleAsyncResolve, @@ -146,6 +192,7 @@ public extension Field where FieldType : Encodable { self.init(name: name, arguments: [argument()], simpleAsyncResolve: function) } + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SimpleAsyncResolve, @@ -156,6 +203,7 @@ public extension Field where FieldType : Encodable { } public extension Field { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SimpleAsyncResolve, @@ -165,6 +213,7 @@ public extension Field { self.init(name: name, arguments: [argument()], simpleAsyncResolve: function) } + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SimpleAsyncResolve, @@ -178,6 +227,7 @@ public extension Field { // MARK: SyncResolve Initializers public extension Field where FieldType : Encodable { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SyncResolve, @@ -185,7 +235,8 @@ public extension Field where FieldType : Encodable { ) { self.init(name: name, arguments: [argument()], syncResolve: function) } - + + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SyncResolve, @@ -196,6 +247,7 @@ public extension Field where FieldType : Encodable { } public extension Field { + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SyncResolve, @@ -205,6 +257,7 @@ public extension Field { self.init(name: name, arguments: [argument()], syncResolve: function) } + @available(*, deprecated, message: "Use the initializer that takes a key path to a `Resolve` function instead.") convenience init( _ name: String, at function: @escaping SyncResolve, @@ -215,15 +268,117 @@ public extension Field { } } +#if compiler(>=5.5) && canImport(_Concurrency) + // MARK: Keypath Initializers +public typealias Resolve = ( + _ context: Context, + _ arguments: Arguments +) async throws -> ResolveType + +@available(macOS 12, *) +public extension Field { + convenience init( + _ name: String, + at keyPath: KeyPath>, + @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] + ) { + let asyncResolve: AsyncResolve = { type in + { context, arguments, group in + let promise = group.next().makePromise(of: FieldType.self) + + promise.completeWithTask { + try await type[keyPath: keyPath](context, arguments) + } + + return promise.futureResult + } + } + + self.init(name: name, arguments: arguments(), asyncResolve: asyncResolve) + } +} + +@available(macOS 12, *) +public extension Field { + convenience init( + _ name: String, + at keyPath: KeyPath>, + as: FieldType.Type, + @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] + ) where ResolveType: Encodable { + let asyncResolve: AsyncResolve = { type in + { context, arguments, group in + let promise = group.next().makePromise(of: ResolveType.self) + + promise.completeWithTask { + try await type[keyPath: keyPath](context, arguments) + } + + return promise.futureResult + } + } + + self.init(name: name, arguments: arguments(), asyncResolve: asyncResolve) + } +} + +@available(macOS 12, *) +public extension Field where Arguments == NoArguments { + convenience init( + _ name: String, + at keyPath: KeyPath> + ) { + let asyncResolve: AsyncResolve = { type in + { context, _, group in + let promise = group.next().makePromise(of: FieldType.self) + + promise.completeWithTask { + try await type[keyPath: keyPath](context, ()) + } + + return promise.futureResult + } + } + + self.init(name: name, arguments: [], asyncResolve: asyncResolve) + } +} + +@available(macOS 12, *) public extension Field where Arguments == NoArguments { + convenience init( + _ name: String, + at keyPath: KeyPath>, + as: FieldType.Type + ) { + let asyncResolve: AsyncResolve = { type in + { context, _, group in + let promise = group.next().makePromise(of: ResolveType.self) + + promise.completeWithTask { + try await type[keyPath: keyPath](context, ()) + } + + return promise.futureResult + } + } + + self.init(name: name, arguments: [], asyncResolve: asyncResolve) + } +} + +#endif + +public extension Field where Arguments == NoArguments, FieldType: Encodable { convenience init( _ name: String, + of type: FieldType.Type = FieldType.self, at keyPath: KeyPath ) { - let syncResolve: SyncResolve = { type in - { context, _ in + let syncResolve: SyncResolve = { type in + { _, _ in type[keyPath: keyPath] } } @@ -233,17 +388,41 @@ public extension Field where Arguments == NoArguments { } public extension Field where Arguments == NoArguments { + @available(*, deprecated, message: "Use the Field.init(_:of:at:) instead.") convenience init( _ name: String, at keyPath: KeyPath, as: FieldType.Type - ) { - let syncResolve: SyncResolve = { type in - return { context, _ in - return type[keyPath: keyPath] + ) where ResolveType: Encodable { + let syncResolve: SyncResolve = { type in + { _, _ in + type[keyPath: keyPath] } } self.init(name: name, arguments: [], syncResolve: syncResolve) } + + convenience init( + _ name: String, + of type: FieldType.Type, + at keyPath: KeyPath + ) where ResolveType: Encodable { + let syncResolve: SyncResolve = { type in + { _, _ in + type[keyPath: keyPath] + } + } + + self.init(name: name, arguments: [], syncResolve: syncResolve) + } +} + +// MARK: Directive + +extension Field { + func directive(_ directive: Directive) -> Field where Directive: FieldDefinitionDirective { + directives.append(directive) + return self + } } diff --git a/Sources/Graphiti/Field/Field/FieldComponent.swift b/Sources/Graphiti/Field/Field/FieldComponent.swift index 6f6f950..4cc8761 100644 --- a/Sources/Graphiti/Field/Field/FieldComponent.swift +++ b/Sources/Graphiti/Field/Field/FieldComponent.swift @@ -1,22 +1,35 @@ import GraphQL -public class FieldComponent { +public class FieldComponent: ExpressibleByStringLiteral { var description: String? = nil var deprecationReason: String? = nil - func field(typeProvider: TypeProvider, coders: Coders) throws -> (String, GraphQLField) { + func field(typeProvider: SchemaTypeProvider, coders: Coders) throws -> (String, GraphQLField) { fatalError() } + + init() {} + + public required init(stringLiteral string: StringLiteralType) { + self.description = string + } } public extension FieldComponent { + @available(*, deprecated, message: "Use a string literal above a component to give it a description.") func description(_ description: String) -> Self { self.description = description return self } + @available(*, deprecated, message: "Use deprecated(reason:).") func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } + + func deprecated(reason deprecationReason: String) -> Self { + self.deprecationReason = deprecationReason + return self + } } diff --git a/Sources/Graphiti/Input/Input.swift b/Sources/Graphiti/Input/Input.swift index f7b3c4b..54ea501 100644 --- a/Sources/Graphiti/Input/Input.swift +++ b/Sources/Graphiti/Input/Input.swift @@ -32,6 +32,18 @@ public final class Input : Compo self.fields = fields super.init(name: name ?? Reflection.name(for: InputObjectType.self)) } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } } public extension Input { diff --git a/Sources/Graphiti/InputField/InputFieldComponent.swift b/Sources/Graphiti/InputField/InputFieldComponent.swift index 25ec993..308225b 100644 --- a/Sources/Graphiti/InputField/InputFieldComponent.swift +++ b/Sources/Graphiti/InputField/InputFieldComponent.swift @@ -9,6 +9,7 @@ public class InputFieldComponent { } public extension InputFieldComponent { + @available(*, deprecated, message: "Use a string literal above a component to give it a description.") func description(_ description: String) -> Self { self.description = description return self diff --git a/Sources/Graphiti/Interface/Interface.swift b/Sources/Graphiti/Interface/Interface.swift index e4f2797..3bde49b 100644 --- a/Sources/Graphiti/Interface/Interface.swift +++ b/Sources/Graphiti/Interface/Interface.swift @@ -2,8 +2,11 @@ import GraphQL public final class Interface : Component { let fields: [FieldComponent] + private var directives: [InterfaceDirective] = [] override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { + applyDirectives() + let interfaceType = try GraphQLInterfaceType( name: name, description: description, @@ -14,7 +17,13 @@ public final class Interface : Component GraphQLFieldMap { + func applyDirectives() { + for directive in directives { + directive.interface(interface: self) + } + } + + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -33,6 +42,18 @@ public final class Interface : Component(_ directive: Directive) -> Interface where Directive: InterfaceDirective { + directives.append(directive) + return self + } +} diff --git a/Sources/Graphiti/Mutation/Mutation.swift b/Sources/Graphiti/Mutation/Mutation.swift index 60e14e6..79528ec 100644 --- a/Sources/Graphiti/Mutation/Mutation.swift +++ b/Sources/Graphiti/Mutation/Mutation.swift @@ -16,7 +16,7 @@ public final class Mutation : Component { ) } - func fields(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -34,6 +34,18 @@ public final class Mutation : Component { self.fields = fields super.init(name: name) } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } } public extension Mutation { diff --git a/Sources/Graphiti/Query/Query.swift b/Sources/Graphiti/Query/Query.swift index 3744a96..5cb9c2b 100644 --- a/Sources/Graphiti/Query/Query.swift +++ b/Sources/Graphiti/Query/Query.swift @@ -8,6 +8,12 @@ public final class Query : Component { } override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { + guard typeProvider.query == nil else { + throw GraphQLError( + message: "Duplicate Query type. There can only be a single Query type in a Schema." + ) + } + typeProvider.query = try GraphQLObjectType( name: name, description: description, @@ -16,7 +22,7 @@ public final class Query : Component { ) } - func fields(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -34,6 +40,18 @@ public final class Query : Component { self.fields = fields super.init(name: name) } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } } public extension Query { diff --git a/Sources/Graphiti/Scalar/Scalar.swift b/Sources/Graphiti/Scalar/Scalar.swift index c834c2a..47a4017 100644 --- a/Sources/Graphiti/Scalar/Scalar.swift +++ b/Sources/Graphiti/Scalar/Scalar.swift @@ -1,14 +1,7 @@ import GraphQL import OrderedCollections -/// Represents a scalar type in the schema. -/// -/// It is **highly** recommended that you do not subclass this type. -/// Instead, modify the encoding/decoding behavior through the `MapEncoder`/`MapDecoder` options available through -/// `Coders` or a custom encoding/decoding on the `ScalarType` itself. -open class Scalar : Component { - // TODO: Change this no longer be an open class - +public final class Scalar : Component { override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { let scalarType = try GraphQLScalarType( name: name, @@ -34,11 +27,11 @@ open class Scalar : Component Map { + public func serialize(scalar: ScalarType, encoder: MapEncoder) throws -> Map { try encoder.encode(scalar) } - open func parse(map: Map, decoder: MapDecoder) throws -> ScalarType { + public func parse(map: Map, decoder: MapDecoder) throws -> ScalarType { try decoder.decode(ScalarType.self, from: map) } @@ -48,6 +41,18 @@ open class Scalar : Component { public let schema: GraphQLSchema - private init( + internal init( coders: Coders, components: [Component] ) throws { @@ -15,7 +16,9 @@ public final class Schema { } guard let query = typeProvider.query else { - fatalError("Query type is required.") + throw GraphQLError( + message: "Query type is required." + ) } self.schema = try GraphQLSchema( @@ -23,17 +26,23 @@ public final class Schema { mutation: typeProvider.mutation, subscription: typeProvider.subscription, types: typeProvider.types, - directives: typeProvider.directives + directives: typeProvider.directives.map(\.0) ) } } +extension Schema: CustomDebugStringConvertible { + public var debugDescription: String { + printSchema(schema: schema) + } +} + public extension Schema { convenience init( coders: Coders = Coders(), @ComponentBuilder _ components: () -> Component - ) throws { - try self.init( + ) { + try! self.init( coders: coders, components: [components()] ) @@ -42,13 +51,14 @@ public extension Schema { convenience init( coders: Coders = Coders(), @ComponentBuilder _ components: () -> [Component] - ) throws { - try self.init( + ) { + try! self.init( coders: coders, components: components() ) } - + + @available(*, deprecated, message: "Use the function where the label for the eventLoopGroup parameter is namded `on`.") func execute( request: String, resolver: Resolver, @@ -56,6 +66,43 @@ public extension Schema { eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil + ) -> EventLoopFuture { + execute( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ) + } + + @available(macOS 12, *) + func execute( + request: String, + resolver: Resolver, + context: Context, + on eventLoopGroup: EventLoopGroup, + variables: [String: Map] = [:], + operationName: String? = nil + ) async throws -> GraphQLResult { + try await execute( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ).get() + } + + func execute( + request: String, + resolver: Resolver, + context: Context, + on eventLoopGroup: EventLoopGroup, + variables: [String: Map] = [:], + operationName: String? = nil ) -> EventLoopFuture { do { return try graphql( @@ -71,7 +118,8 @@ public extension Schema { return eventLoopGroup.next().makeFailedFuture(error) } } - + + @available(*, deprecated, message: "Use the function where the label for the eventLoopGroup parameter is named `on`.") func subscribe( request: String, resolver: Resolver, @@ -79,6 +127,43 @@ public extension Schema { eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil + ) -> EventLoopFuture { + subscribe( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ) + } + + @available(macOS 12, *) + func subscribe( + request: String, + resolver: Resolver, + context: Context, + on eventLoopGroup: EventLoopGroup, + variables: [String: Map] = [:], + operationName: String? = nil + ) async throws -> SubscriptionResult { + try await self.subscribe( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ).get() + } + + func subscribe( + request: String, + resolver: Resolver, + context: Context, + on eventLoopGroup: EventLoopGroup, + variables: [String: Map] = [:], + operationName: String? = nil ) -> EventLoopFuture { do { return try graphqlSubscribe( diff --git a/Sources/Graphiti/Schema/SchemaTypeProvider.swift b/Sources/Graphiti/Schema/SchemaTypeProvider.swift index da9dd12..99a47a1 100644 --- a/Sources/Graphiti/Schema/SchemaTypeProvider.swift +++ b/Sources/Graphiti/Schema/SchemaTypeProvider.swift @@ -1,6 +1,6 @@ import GraphQL -final class SchemaTypeProvider : TypeProvider { +final class SchemaTypeProvider: TypeProvider { var graphQLTypeMap: [AnyType: GraphQLType] = [ AnyType(Int.self): GraphQLInt, AnyType(Double.self): GraphQLFloat, @@ -12,6 +12,6 @@ final class SchemaTypeProvider : TypeProvider { var mutation: GraphQLObjectType? = nil var subscription: GraphQLObjectType? = nil var types: [GraphQLNamedType] = [] - var directives: [GraphQLDirective] = [] + var directives: [(GraphQLDirective, (Map, Coders) throws -> Any)] = [] } diff --git a/Sources/Graphiti/Subscription/SubscribeField.swift b/Sources/Graphiti/Subscription/SubscribeField.swift index 48a7dde..3eb270f 100644 --- a/Sources/Graphiti/Subscription/SubscribeField.swift +++ b/Sources/Graphiti/Subscription/SubscribeField.swift @@ -9,7 +9,7 @@ public class SubscriptionField let subscribe: AsyncResolve> - override func field(typeProvider: TypeProvider, coders: Coders) throws -> (String, GraphQLField) { + override func field(typeProvider: SchemaTypeProvider, coders: Coders) throws -> (String, GraphQLField) { let field = GraphQLField( type: try typeProvider.getOutputType(from: FieldType.self, field: name), description: description, @@ -65,6 +65,7 @@ public class SubscriptionField( @@ -171,6 +172,18 @@ public class SubscriptionField : Component GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -34,6 +34,18 @@ public final class Subscription : Component : Component { +public final class Type : Component where ObjectType: Encodable { let interfaces: [Any.Type] let fields: [FieldComponent] + private var directives: [ObjectDirective] = [] let isTypeOf: GraphQLIsTypeOf = { source, _, _ in - return source is ObjectType + source is ObjectType } override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { + applyDirectives() + let objectType = try GraphQLObjectType( name: name, description: description, - fields: fields(typeProvider: typeProvider, coders: coders), - interfaces: interfaces.map { + fields: try fields(typeProvider: typeProvider, coders: coders), + interfaces: try interfaces.map { try typeProvider.getInterfaceType(from: $0) }, isTypeOf: isTypeOf @@ -22,7 +25,7 @@ public final class Type : Component GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -33,6 +36,12 @@ public final class Type : Component : Component _ fields: () -> FieldComponent ) { self.init( @@ -60,10 +82,11 @@ public extension Type { ) } + @available(*, deprecated, message: "Use the initializer where the label for the interfaces parameter is named `implements`.") convenience init( _ type: ObjectType.Type, as name: String? = nil, - interfaces: [Any.Type] = [], + interfaces: [Any.Type], @FieldComponentBuilder _ fields: () -> [FieldComponent] ) { self.init( @@ -74,3 +97,42 @@ public extension Type { ) } } + +public extension Type { + convenience init( + _ type: ObjectType.Type, + as name: String? = nil, + implements interfaces: Any.Type..., + @FieldComponentBuilder fields: () -> FieldComponent + ) { + self.init( + type: type, + name: name, + interfaces: interfaces, + fields: [fields()] + ) + } + + convenience init( + _ type: ObjectType.Type, + as name: String? = nil, + implements interfaces: Any.Type..., + @FieldComponentBuilder fields: () -> [FieldComponent] + ) { + self.init( + type: type, + name: name, + interfaces: interfaces, + fields: fields() + ) + } +} + +// MARK: Directive + +extension Type { + func directive(_ directive: Directive) -> Type where Directive: ObjectDirective { + directives.append(directive) + return self + } +} diff --git a/Sources/Graphiti/Types/Types.swift b/Sources/Graphiti/Types/Types.swift index 57c6072..dd9be16 100644 --- a/Sources/Graphiti/Types/Types.swift +++ b/Sources/Graphiti/Types/Types.swift @@ -13,6 +13,18 @@ public final class Types : Component { self.types = types super.init(name: "") } + + public required init(extendedGraphemeClusterLiteral string: String) { + fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented") + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") + } + + public required init(unicodeScalarLiteral string: String) { + fatalError("init(unicodeScalarLiteral:) has not been implemented") + } } public extension Types { diff --git a/Sources/Graphiti/Union/Union.swift b/Sources/Graphiti/Union/Union.swift index a334355..0286688 100644 --- a/Sources/Graphiti/Union/Union.swift +++ b/Sources/Graphiti/Union/Union.swift @@ -23,7 +23,18 @@ public final class Union : Component: ExpressibleByStringLiteral { + var description: String? = nil + + func enumValue(typeProvider: SchemaTypeProvider, coders: Coders) throws -> (String, GraphQLEnumValue) { + fatalError() + } + + init() {} + + public required init(stringLiteral string: StringLiteralType) { + self.description = string + } +} diff --git a/Sources/Graphiti/Value/Value.swift b/Sources/Graphiti/Value/Value.swift index c934afb..e982cb3 100644 --- a/Sources/Graphiti/Value/Value.swift +++ b/Sources/Graphiti/Value/Value.swift @@ -1,12 +1,29 @@ -public final class Value where EnumType.RawValue == String { +import GraphQL + +#warning("TODO: Rename to EnumValue") +public final class Value: EnumValueComponent where EnumType: Encodable & RawRepresentable, EnumType.RawValue == String { let value: EnumType - var description: String? var deprecationReason: String? + override func enumValue(typeProvider: SchemaTypeProvider, coders: Coders) throws -> (String, GraphQLEnumValue) { + let enumValue = GraphQLEnumValue( + value: try MapEncoder().encode(value), + description: description, + deprecationReason: deprecationReason + ) + + return (value.rawValue, enumValue) + } + init( value: EnumType ) { self.value = value + super.init() + } + + public required init(stringLiteral string: StringLiteralType) { + fatalError("init(stringLiteral:) has not been implemented") } } @@ -15,15 +32,20 @@ public extension Value { self.init(value: value) } - @discardableResult + @available(*, deprecated, message: "Use a string literal above a component to give it a description.") func description(_ description: String) -> Self { self.description = description return self } - @discardableResult + @available(*, deprecated, message: "Use deprecated(reason:).") func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } + + func deprecated(reason deprecationReason: String) -> Self { + self.deprecationReason = deprecationReason + return self + } } diff --git a/Sources/Graphiti/Value/ValueBuilder.swift b/Sources/Graphiti/Value/ValueBuilder.swift index ceba725..f7a4162 100644 --- a/Sources/Graphiti/Value/ValueBuilder.swift +++ b/Sources/Graphiti/Value/ValueBuilder.swift @@ -1,10 +1,10 @@ @resultBuilder -public struct ValueBuilder where EnumType.RawValue == String { - public static func buildExpression(_ value: Value) -> Value { +public struct ValueBuilder where EnumType: Encodable & RawRepresentable, EnumType.RawValue == String { + public static func buildExpression(_ value: EnumValueComponent) -> EnumValueComponent { value } - public static func buildBlock(_ value: Value...) -> [Value] { + public static func buildBlock(_ value: EnumValueComponent...) -> [EnumValueComponent] { value } } diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift new file mode 100644 index 0000000..ff0d60d --- /dev/null +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -0,0 +1,415 @@ +import XCTest +import GraphQL +import NIO +@testable import Graphiti + +@available(macOS 12, *) +struct Count: Encodable { + var value: Int +} + +@available(macOS 12, *) +actor CounterState { + var count: Count + + init(count: Count) { + self.count = count + } + + func increment() -> Count { + count.value += 1 + return count + } + + func decrement() -> Count { + count.value -= 1 + return count + } + + func increment(by amount: Int) -> Count { + count.value += amount + return count + } + + func decrement(by amount: Int) -> Count { + count.value -= amount + return count + } +} + +@available(macOS 12, *) +struct CounterContext { + var count: () async -> Count + var increment: () async -> Count + var decrement: () async -> Count + var incrementBy: (_ amount: Int) async -> Count + var decrementBy: (_ amount: Int) async -> Count +} + +@available(macOS 12, *) +struct CounterResolver { + var count: (CounterContext, Void) async throws -> Count + var increment: (CounterContext, Void) async throws -> Count + var decrement: (CounterContext, Void) async throws -> Count + + struct IncrementByArguments: Decodable { + let amount: Int + } + + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Count + + struct DecrementByArguments: Decodable { + let amount: Int + } + + var decrementBy: (CounterContext, DecrementByArguments) async throws -> Count +} + +@available(macOS 12, *) +struct CounterAPI { + let schema = Schema { + "Description" + Type(Count.self) { + Field("value", at: \.value) + } + + Query { + Field("count", at: \.count) + } + + Mutation { + Field("increment", at: \.increment) + Field("decrement", at: \.decrement) + + Field("incrementBy", at: \.incrementBy) { + Argument("amount", at: \.amount) + } + + Field("decrementBy", at: \.decrementBy) { + Argument("amount", at: \.amount) + } + } + } +} + +@available(macOS 12, *) +extension CounterContext { + static let live: CounterContext = { + let count = Count(value: 0) + let state = CounterState(count: count) + + return CounterContext( + count: { + await state.count + }, + increment: { + await state.increment() + }, + decrement: { + await state.decrement() + }, + incrementBy: { count in + await state.increment(by: count) + }, + decrementBy: { count in + await state.decrement(by: count) + } + ) + }() +} + +@available(macOS 12, *) +extension CounterResolver { + static let live = CounterResolver( + count: { context, _ in + await context.count() + }, + increment: { context, _ in + await context.increment() + }, + decrement: { context, _ in + await context.decrement() + }, + incrementBy: { context, arguments in + await context.incrementBy(arguments.amount) + }, + decrementBy: { context, arguments in + await context.decrementBy(arguments.amount) + } + ) +} + +#warning("TODO: Move this to GraphQL") +extension GraphQLResult: CustomDebugStringConvertible { + public var debugDescription: String { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let data = try! encoder.encode(self) + return String(data: data, encoding: .utf8)! + } +} + + + +@available(macOS 12, *) +class CounterTests: XCTestCase { + private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + + deinit { + try? self.group.syncShutdownGracefully() + } + + func testDirective() throws { + struct User: Encodable { + let name: String + } + + struct Context { + + } + + struct Resolver { + let user: User + } + + struct DelegateField: Decodable, ObjectDirective, InterfaceDirective { + let name: String + + func object(object: Object) where ObjectType: Encodable { +// object.fields.append(Graphiti.Field.init( +// name: name, +// arguments: [], +// resolve: { object in +// { _, _, group in +// group.next().makeCompletedFuture(.success("Yo")) +// } +// } +// )) + } + + func interface(interface: Interface) { + + } + } + + #warning("TODO: Move Decodable requirement from ArgumentComponent to Argument") + struct Lowercased: Decodable, FieldDefinitionDirective, FieldDirective { + func fieldDefinition(field: Graphiti.Field) where Arguments: Decodable { + let resolve = field.resolve + + field.resolve = { object in + { context, arguments, group in + try resolve(object)(context, arguments, group).map { value in + guard let string = value as? String else { + return value + } + + return string.lowercased() + } + } + } + } + + func field(resolve: @escaping AsyncResolve) -> AsyncResolve { + { object in + { context, arguments, group in + try resolve(object)(context, arguments, group).map { value in + guard let string = value as? String else { + return value + } + + return string.lowercased() + } + } + } + } + } + + struct Uppercased: Decodable, FieldDefinitionDirective, FieldDirective { + func fieldDefinition(field: Graphiti.Field) where Arguments: Decodable { + let resolve = field.resolve + + field.resolve = { object in + { context, arguments, group in + try resolve(object)(context, arguments, group).map { value in + guard let string = value as? String else { + return value + } + + return string.uppercased() + } + } + } + } + + func field(resolve: @escaping AsyncResolve) -> AsyncResolve { + { object in + { context, arguments, group in + try resolve(object)(context, arguments, group).map { value in + guard let string = value as? String else { + return value + } + + return string.uppercased() + } + } + } + } + } + + let schema = Schema { + Directive(DelegateField.self) { + Argument("name", of: String.self, at: \.name) + } on: { + DirectiveLocation(.object) + DirectiveLocation(.interface) + } + #warning("TODO: Implement repeatable directives") +// .repeatable() + + Directive(Lowercased.self) { + DirectiveLocation(.fieldDefinition) + DirectiveLocation(.field) + } + + Directive(Uppercased.self) { + DirectiveLocation(.fieldDefinition) + DirectiveLocation(.field) + } + + Type(User.self) { + Field("name", of: String.self, at: \.name) +// .directive(Lowercased()) +// .directive(Uppercased()) + } + .directive(DelegateField(name: "salute")) + + Query { + Field.init("user", of: User.self, at: \.user) + } + } + + #warning("TODO: Add directives property to all types and print them in printSchema") + debugPrint(schema) + + let result = try schema.execute( + request: "query { user { name @lowercased @uppercased } }", + resolver: Resolver(user: User(name: "Paulo")), + context: Context(), + on: group + ).wait() + + + debugPrint(result) + } + + func testCounter() throws { + let api = CounterAPI() + + var query = """ + query { + count { + value + } + } + """ + + var expected = GraphQLResult(data: ["count": ["value": 0]]) + + var result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + debugPrint(result) + XCTAssertEqual(result, expected) + + query = """ + mutation { + increment { + value + } + } + """ + expected = GraphQLResult(data: ["increment": ["value": 1]]) + + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + debugPrint(result) + XCTAssertEqual(result, expected) + + query = """ + mutation { + decrement { + value + } + } + """ + expected = GraphQLResult(data: ["decrement": ["value": 0]]) + + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + debugPrint(result) + XCTAssertEqual(result, expected) + + query = """ + mutation { + incrementBy(amount: 5) { + value + } + } + """ + expected = GraphQLResult(data: ["incrementBy": ["value": 5]]) + + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + debugPrint(result) + XCTAssertEqual(result, expected) + + query = """ + mutation { + decrementBy(amount: 5) { + value + } + } + """ + expected = GraphQLResult(data: ["decrementBy": ["value": 0]]) + + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + debugPrint(result) + XCTAssertEqual(result, expected) + } +} + +@available(macOS 12, *) +extension CounterTests { + static var allTests: [(String, (CounterTests) -> () throws -> Void)] { + return [ + ("testCounter", testCounter), + ] + } +} + diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift index 87c7172..b7c6b82 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift @@ -3,7 +3,7 @@ import GraphQL import NIO @testable import Graphiti -struct ID : Codable { +struct ID: Codable { let id: String init(_ id: String) { @@ -21,12 +21,13 @@ struct ID : Codable { } } -struct User : Codable { - let id: String +@available(macOS 12, *) +struct User: Codable { + let id: ID let name: String? let friends: [User]? - init(id: String, name: String?, friends: [User]?) { + init(id: ID, name: String?, friends: [User]?) { self.id = id self.name = name self.friends = friends @@ -35,81 +36,60 @@ struct User : Codable { init(_ input: UserInput) { self.id = input.id self.name = input.name + if let friends = input.friends { - self.friends = friends.map{ User($0) } + self.friends = friends.map(User.init) } else { self.friends = nil } } - - func toEvent(context: HelloContext, arguments: NoArguments) throws -> UserEvent { - return UserEvent(user: self) - } } -struct UserInput : Codable { - let id: String +struct UserInput: Codable { + let id: ID let name: String? let friends: [UserInput]? } -struct UserEvent : Codable { +@available(macOS 12, *) +struct UserEvent: Codable { let user: User } +@available(macOS 12, *) final class HelloContext { - func hello() -> String { + func hello() async -> String { "world" } } +@available(macOS 12, *) struct HelloResolver { - func hello(context: HelloContext, arguments: NoArguments) -> String { - context.hello() - } - - func asyncHello( - context: HelloContext, - arguments: NoArguments, - group: EventLoopGroup - ) -> EventLoopFuture { - group.next().makeSucceededFuture(context.hello()) - } + var hello: Resolve - struct FloatArguments : Codable { + struct FloatArguments: Codable { let float: Float } - func getFloat(context: HelloContext, arguments: FloatArguments) -> Float { - arguments.float - } + var getFloat: Resolve - struct IDArguments : Codable { + struct IDArguments: Codable { let id: ID } - func getId(context: HelloContext, arguments: IDArguments) -> ID { - arguments.id - } - - func getUser(context: HelloContext, arguments: NoArguments) -> User { - User(id: "123", name: "John Doe", friends: nil) - } + var getId: Resolve + var getUser: Resolve - struct AddUserArguments : Codable { + struct AddUserArguments: Codable { let user: UserInput } - func addUser(context: HelloContext, arguments: AddUserArguments) -> User { - User(arguments.user) - } + var addUser: Resolve } -struct HelloAPI : API { - let resolver = HelloResolver() - let context = HelloContext() - - let schema = try! Schema { +@available(macOS 12, *) +struct HelloAPI { + let schema = Schema { Scalar(Float.self) .description("The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).") @@ -133,29 +113,55 @@ struct HelloAPI : API { } Query { - Field("hello", at: HelloResolver.hello) - Field("asyncHello", at: HelloResolver.asyncHello) + Field("hello", at: \.hello) - Field("float", at: HelloResolver.getFloat) { + Field("float", at: \.getFloat) { Argument("float", at: \.float) } - Field("id", at: HelloResolver.getId) { + Field("id", at: \.getId) { Argument("id", at: \.id) } - Field("user", at: HelloResolver.getUser) + Field("user", at: \.getUser) } Mutation { - Field("addUser", at: HelloResolver.addUser) { + Field("addUser", at: \.addUser) { Argument("user", at: \.user) } } } } -class HelloWorldTests : XCTestCase { +@available(macOS 12, *) +extension HelloContext { + static let test = HelloContext() +} + +@available(macOS 12, *) +extension HelloResolver { + static let test = HelloResolver( + hello: { context, _ in + await context.hello() + }, + getFloat: { _, arguments in + arguments.float + }, + getId: { _, arguments in + arguments.id + }, + getUser: { _, _ in + User(id: ID("123"), name: "John Doe", friends: nil) + }, + addUser: { _, arguments in + User(arguments.user) + } + ) +} + +@available(macOS 12, *) +class HelloWorldTests: XCTestCase { private let api = HelloAPI() private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) @@ -166,30 +172,12 @@ class HelloWorldTests : XCTestCase { func testHello() throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) - let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, - on: group - ).whenSuccess { result in - XCTAssertEqual(result, expected) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 10) - } - - func testHelloAsync() throws { - let query = "{ asyncHello }" - let expected = GraphQLResult(data: ["asyncHello": "world"]) - - let expectation = XCTestExpectation() - - api.execute( - request: query, - context: api.context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -213,9 +201,10 @@ class HelloWorldTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -231,15 +220,16 @@ class HelloWorldTests : XCTestCase { query = """ query Query($float: Float!) { - float(float: $float) + float(float: $float) } """ let expectationA = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, + resolver: .test, + context: .test, on: group, variables: ["float": 4] ).whenSuccess { result in @@ -251,15 +241,16 @@ class HelloWorldTests : XCTestCase { query = """ query Query { - float(float: 4) + float(float: 4) } """ let expectationB = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -270,17 +261,17 @@ class HelloWorldTests : XCTestCase { query = """ query Query($id: String!) { - id(id: $id) + id(id: $id) } """ expected = GraphQLResult(data: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"]) - let expectationC = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, + resolver: .test, + context: .test, on: group, variables: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"] ).whenSuccess { result in @@ -292,15 +283,16 @@ class HelloWorldTests : XCTestCase { query = """ query Query { - id(id: "85b8d502-8190-40ab-b18f-88edd297d8b6") + id(id: "85b8d502-8190-40ab-b18f-88edd297d8b6") } """ let expectationD = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: api.context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -320,17 +312,18 @@ class HelloWorldTests : XCTestCase { } """ - let variables: [String: Map] = ["user" : [ "id" : "123", "name" : "bob" ]] + let variables: [String: Map] = ["user": ["id": "123", "name": "bob"]] let expected = GraphQLResult( - data: ["addUser" : [ "id" : "123", "name" : "bob" ]] + data: ["addUser": ["id": "123", "name": "bob"]] ) let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: mutation, - context: api.context, + resolver: .test, + context: .test, on: group, variables: variables ).whenSuccess { result in @@ -355,17 +348,18 @@ class HelloWorldTests : XCTestCase { } """ - let variables: [String: Map] = ["user" : [ "id" : "123", "name" : "bob", "friends": [["id": "124", "name": "jeff"]]]] + let variables: [String: Map] = ["user": ["id": "123", "name": "bob", "friends": [["id": "124", "name": "jeff"]]]] let expected = GraphQLResult( - data: ["addUser" : [ "id" : "123", "name" : "bob", "friends": [["id": "124", "name": "jeff"]]]] + data: ["addUser": ["id": "123", "name": "bob", "friends": [["id": "124", "name": "jeff"]]]] ) let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: mutation, - context: api.context, + resolver: .test, + context: .test, on: group, variables: variables ).whenSuccess { result in @@ -377,11 +371,11 @@ class HelloWorldTests : XCTestCase { } } +@available(macOS 12, *) extension HelloWorldTests { static var allTests: [(String, (HelloWorldTests) -> () throws -> Void)] { return [ ("testHello", testHello), - ("testHelloAsync", testHelloAsync), ("testBoyhowdy", testBoyhowdy), ("testScalar", testScalar), ("testInput", testInput), diff --git a/Tests/GraphitiTests/ScalarTests.swift b/Tests/GraphitiTests/ScalarTests.swift index e291e38..f979c80 100644 --- a/Tests/GraphitiTests/ScalarTests.swift +++ b/Tests/GraphitiTests/ScalarTests.swift @@ -4,38 +4,40 @@ import Foundation import NIO @testable import Graphiti -class ScalarTests : XCTestCase { +@available(macOS 12, *) +class ScalarTests: XCTestCase { // MARK: Test UUID converts to String as expected func testUUIDOutput() throws { - struct UUIDOutput : Codable { + struct UUIDOutput: Codable { let value: UUID } - struct TestResolver { - func uuid(context: NoContext, arguments: NoArguments) -> UUIDOutput { - return UUIDOutput(value: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!) + struct Resolver { + let uuid: (Void, Void) async throws -> UUIDOutput = { _, _ in + UUIDOutput(value: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!) } } - let testSchema = try Schema { + let schema = Schema { Scalar(UUID.self, as: "UUID") + Type(UUIDOutput.self) { Field("value", at: \.value) } + Query { - Field("uuid", at: TestResolver.uuid) + Field("uuid", at: \.uuid) } } - let api = TestAPI ( - resolver: TestResolver(), - schema: testSchema - ) let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } + + defer { + try? group.syncShutdownGracefully() + } XCTAssertEqual( - try api.execute( + try schema.execute( request: """ query { uuid { @@ -43,7 +45,8 @@ class ScalarTests : XCTestCase { } } """, - context: NoContext(), + resolver: Resolver(), + context: (), on: group ).wait(), GraphQLResult(data: [ @@ -55,41 +58,39 @@ class ScalarTests : XCTestCase { } func testUUIDArg() throws { - struct UUIDOutput : Codable { + struct UUIDOutput: Codable { let value: UUID } - struct Arguments : Codable { + struct Arguments: Codable { let value: UUID } - struct TestResolver { - func uuid(context: NoContext, arguments: Arguments) -> UUIDOutput { - return UUIDOutput(value: arguments.value) + struct Resolver { + let uuid: (Void, Arguments) async throws -> UUIDOutput = { _, arguments in + UUIDOutput(value: arguments.value) } } - let testSchema = try Schema { + let schema = Schema { Scalar(UUID.self, as: "UUID") + Type(UUIDOutput.self) { Field("value", at: \.value) } + Query { - Field("uuid", at: TestResolver.uuid) { + Field("uuid", at: \.uuid) { Argument("value", at: \.value) } } } - let api = TestAPI ( - resolver: TestResolver(), - schema: testSchema - ) let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) defer { try? group.syncShutdownGracefully() } XCTAssertEqual( - try api.execute( + try schema.execute( request: """ query { uuid (value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") { @@ -97,7 +98,8 @@ class ScalarTests : XCTestCase { } } """, - context: NoContext(), + resolver: Resolver(), + context: (), on: group ).wait(), GraphQLResult(data: [ @@ -109,48 +111,50 @@ class ScalarTests : XCTestCase { } func testUUIDInput() throws { - struct UUIDOutput : Codable { + struct UUIDOutput: Codable { let value: UUID } - struct UUIDInput : Codable { + struct UUIDInput: Codable { let value: UUID } - struct Arguments : Codable { + struct Arguments: Codable { let input: UUIDInput } - struct TestResolver { - func uuid(context: NoContext, arguments: Arguments) -> UUIDOutput { - return UUIDOutput(value: arguments.input.value) + struct Resolver { + let uuid: (Void, Arguments) async throws -> UUIDOutput = { _, arguments in + UUIDOutput(value: arguments.input.value) } } - let testSchema = try Schema { + let schema = Schema { Scalar(UUID.self, as: "UUID") + Type(UUIDOutput.self) { Field("value", at: \.value) } + Input(UUIDInput.self) { InputField("value", at: \.value) } + Query { - Field("uuid", at: TestResolver.uuid) { + Field("uuid", at: \.uuid) { Argument("input", at: \.input) } } } - let api = TestAPI ( - resolver: TestResolver(), - schema: testSchema - ) let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } + + defer { + try? group.syncShutdownGracefully() + } XCTAssertEqual( - try api.execute( + try schema.execute( request: """ query { uuid (input: {value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"}) { @@ -158,7 +162,8 @@ class ScalarTests : XCTestCase { } } """, - context: NoContext(), + resolver: Resolver(), + context: (), on: group ).wait(), GraphQLResult(data: [ @@ -184,7 +189,7 @@ class ScalarTests : XCTestCase { let coders = Coders() coders.decoder.dateDecodingStrategy = .iso8601 coders.encoder.dateEncodingStrategy = .iso8601 - let testSchema = try Schema( + let testSchema = Schema( coders: coders ) { Scalar(Date.self, as: "Date") @@ -195,6 +200,7 @@ class ScalarTests : XCTestCase { Field("date", at: TestResolver.date) } } + let api = TestAPI ( resolver: TestResolver(), schema: testSchema @@ -241,7 +247,7 @@ class ScalarTests : XCTestCase { let coders = Coders() coders.decoder.dateDecodingStrategy = .iso8601 coders.encoder.dateEncodingStrategy = .iso8601 - let testSchema = try Schema( + let testSchema = Schema( coders: coders ) { Scalar(Date.self, as: "Date") @@ -304,7 +310,7 @@ class ScalarTests : XCTestCase { let coders = Coders() coders.decoder.dateDecodingStrategy = .iso8601 coders.encoder.dateEncodingStrategy = .iso8601 - let testSchema = try Schema( + let testSchema = Schema( coders: coders ) { Scalar(Date.self, as: "Date") @@ -360,7 +366,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(StringCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -412,7 +418,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(StringCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -470,7 +476,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(StringCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -524,7 +530,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(DictCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -580,7 +586,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(DictCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -642,7 +648,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(DictCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) @@ -689,7 +695,7 @@ class ScalarTests : XCTestCase { } } -fileprivate class TestAPI : API { +fileprivate class TestAPI: API { public let resolver: Resolver public let schema: Schema diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index c33f558..274654d 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -1,103 +1,106 @@ import Graphiti -public struct StarWarsAPI : API { - public let resolver = StarWarsResolver() - - public let schema = try! Schema { +@available(macOS 12, *) +public struct StarWarsAPI { + public let schema = Schema { + "One of the films in the Star Wars Trilogy." Enum(Episode.self) { + "Released in 1977." Value(.newHope) - .description("Released in 1977.") + "Released in 1980." Value(.empire) - .description("Released in 1980.") + "Released in 1983." Value(.jedi) - .description("Released in 1983.") } - .description("One of the films in the Star Wars Trilogy.") + "A character in the Star Wars Trilogy." Interface(Character.self) { - Field("id", at: \.id) - .description("The id of the character.") + "The id of the character." + Field("id", of: String.self, at: \.id) - Field("name", at: \.name) - .description("The name of the character.") + "The name of the character." + Field("name", of: String.self, at: \.name) - Field("friends", at: \.friends, as: [TypeReference].self) - .description("The friends of the character, or an empty list if they have none.") + "The friends of the character, or an empty list if they have none." + Field("friends", of: [TypeReference].self, at: \.friends) - Field("appearsIn", at: \.appearsIn) - .description("Which movies they appear in.") + "Which movies they appear in." + Field("appearsIn", of: [Episode].self, at: \.appearsIn) - Field("secretBackstory", at: \.secretBackstory) - .description("All secrets about their past.") + "All secrets about their past." + Field("secretBackstory", of: String?.self, at: \.secretBackstory) } - .description("A character in the Star Wars Trilogy.") + "A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY." Type(Planet.self) { - Field("id", at: \.id) - Field("name", at: \.name) - Field("diameter", at: \.diameter) - Field("rotationPeriod", at: \.rotationPeriod) - Field("orbitalPeriod", at: \.orbitalPeriod) - Field("residents", at: \.residents, as: [TypeReference].self) + Field("id", of: String.self, at: \.id) + Field("name", of: String.self, at: \.name) + Field("diameter", of: Int.self, at: \.diameter) + Field("rotationPeriod", of: Int.self, at: \.rotationPeriod) + Field("orbitalPeriod", of: Int.self, at: \.orbitalPeriod) + Field.init("residents", of: [TypeReference].self, at: \.residents) } - .description("A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY.") - Type(Human.self, interfaces: [Character.self]) { - Field("id", at: \.id) - Field("name", at: \.name) - Field("appearsIn", at: \.appearsIn) - Field("homePlanet", at: \.homePlanet) + "A humanoid creature in the Star Wars universe." + Type(Human.self, implements: Character.self) { + Field("id", of: String.self, at: \.id) + Field("name", of: String.self, at: \.name) + Field("appearsIn", of: [Episode].self, at: \.appearsIn) + Field("homePlanet", of: Planet.self, at: \.homePlanet) - Field("friends", at: Human.getFriends, as: [Character].self) - .description("The friends of the human, or an empty list if they have none.") + "The friends of the human, or an empty list if they have none." + Field("friends", at: \.getFriends) - Field("secretBackstory", at: Human.getSecretBackstory) - .description("Where are they from and how they came to be who they are.") + "Where are they from and how they came to be who they are." + Field("secretBackstory", at: \.getSecretBackstory) } - .description("A humanoid creature in the Star Wars universe.") - Type(Droid.self, interfaces: [Character.self]) { - Field("id", at: \.id) - Field("name", at: \.name) - Field("appearsIn", at: \.appearsIn) - Field("primaryFunction", at: \.primaryFunction) + "A mechanical creature in the Star Wars universe." + Type(Droid.self, implements: Character.self) { + Field("id", of: String.self, at: \.id) + Field("name", of: String.self, at: \.name) + Field("appearsIn", of: [Episode].self, at: \.appearsIn) + Field("primaryFunction", of: String.self, at: \.primaryFunction) - Field("friends", at: Droid.getFriends, as: [Character].self) - .description("The friends of the droid, or an empty list if they have none.") + "The friends of the droid, or an empty list if they have none." + Field("friends", at: \.getFriends) - Field("secretBackstory", at: Droid.getSecretBackstory) - .description("Where are they from and how they came to be who they are.") + "Where are they from and how they came to be who they are." + Field("secretBackstory", at: \.getSecretBackstory) } - .description("A mechanical creature in the Star Wars universe.") Union(SearchResult.self, members: Planet.self, Human.self, Droid.self) Query { - Field("hero", at: StarWarsResolver.hero, as: Character.self) { + "Returns a hero based on the given episode." + Field("hero", at: \.hero) { + """ + If omitted, returns the hero of the whole saga. + If provided, returns the hero of that particular episode. + """ Argument("episode", at: \.episode) - .description("If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.") } - .description("Returns a hero based on the given episode.") - - Field("human", at: StarWarsResolver.human) { + Field("human", at: \.human) { + "Id of the human." Argument("id", at: \.id) - .description("Id of the human.") } - Field("droid", at: StarWarsResolver.droid) { + Field("droid", at: \.droid) { + "Id of the droid." Argument("id", at: \.id) - .description("Id of the droid.") } - Field("search", at: StarWarsResolver.search, as: [SearchResult].self) { + Field("search", at: \.search) { Argument("query", at: \.query) .defaultValue("R2-D2") } } - Types(Human.self, Droid.self) + #warning("TODO: Automatically add all types instead of having to manually define them here.") + Types(Human.self, Droid.self, Planet.self) } } + diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift index f7de494..12f4c27 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift @@ -5,7 +5,8 @@ * fetching this data from a backend service rather than from hardcoded * values in a more complex demo. */ -public final class StarWarsContext { +@available(macOS 12.0.0, *) +public actor StarWarsContext { private static var tatooine = Planet( id:"10001", name: "Tatooine", @@ -98,8 +99,6 @@ public final class StarWarsContext { "2001": r2d2, ] - public init() {} - /** * Helper function to get a character by ID. */ @@ -146,7 +145,7 @@ public final class StarWarsContext { * Allows us to get the secret backstory, or not. */ public func getSecretBackStory() throws -> String? { - struct Secret : Error, CustomStringConvertible { + struct Secret: Error, CustomStringConvertible { let description: String } @@ -187,6 +186,11 @@ public final class StarWarsContext { * Allows us to query for either a Human, Droid, or Planet. */ public func search(query: String) -> [SearchResult] { - return getPlanets(query: query) + getHumans(query: query) + getDroids(query: query) + getPlanets(query: query) + getHumans(query: query) + getDroids(query: query) } } + +@available(macOS 12.0.0, *) +public extension StarWarsContext { + static let live = StarWarsContext() +} diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift index a5d6a2e..6468769 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift @@ -1,19 +1,19 @@ -public enum Episode : String, Codable, CaseIterable { +public enum Episode: String, Codable, CaseIterable { case newHope = "NEWHOPE" case empire = "EMPIRE" case jedi = "JEDI" } -public protocol Character : Codable { +public protocol Character: Codable { var id: String { get } var name: String { get } var friends: [String] { get } var appearsIn: [Episode] { get } } -public protocol SearchResult : Codable {} +public protocol SearchResult: Codable {} -public struct Planet : SearchResult, Codable { +public struct Planet: SearchResult, Codable { public let id: String public let name: String public let diameter: Int @@ -22,7 +22,7 @@ public struct Planet : SearchResult, Codable { public var residents: [Human] } -public struct Human : Character, SearchResult, Codable { +public struct Human: Character, SearchResult, Codable { public let id: String public let name: String public let friends: [String] @@ -30,7 +30,7 @@ public struct Human : Character, SearchResult, Codable { public let homePlanet: Planet } -public struct Droid : Character, SearchResult, Codable { +public struct Droid: Character, SearchResult, Codable { public let id: String public let name: String public let friends: [String] diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift index f84c950..a3b985b 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift @@ -1,67 +1,83 @@ import Graphiti +@available(macOS 12.0.0, *) extension Character { public var secretBackstory: String? { nil } - - public func getFriends(context: StarWarsContext, arguments: NoArguments) -> [Character] { - [] - } } +@available(macOS 12.0.0, *) extension Human { - public func getFriends(context: StarWarsContext, arguments: NoArguments) -> [Character] { - context.getFriends(of: self) + public var getFriends: (StarWarsContext, Void) async throws -> [Character] { + return { context, _ in + await context.getFriends(of: self) + } } - public func getSecretBackstory(context: StarWarsContext, arguments: NoArguments) throws -> String? { - try context.getSecretBackStory() + public var getSecretBackstory: (StarWarsContext, Void) async throws -> String? { + return { context, _ in + try await context.getSecretBackStory() + } } } +@available(macOS 12.0.0, *) extension Droid { - public func getFriends(context: StarWarsContext, arguments: NoArguments) -> [Character] { - context.getFriends(of: self) + public var getFriends: (StarWarsContext, Void) async throws -> [Character] { + return { context, _ in + await context.getFriends(of: self) + } } - public func getSecretBackstory(context: StarWarsContext, arguments: NoArguments) throws -> String? { - try context.getSecretBackStory() + public var getSecretBackstory: (StarWarsContext, Void) async throws -> String? { + return { context, _ in + try await context.getSecretBackStory() + } } } +@available(macOS 12.0.0, *) public struct StarWarsResolver { - public init() {} - - public struct HeroArguments : Codable { + public struct HeroArguments: Codable { public let episode: Episode? } - public func hero(context: StarWarsContext, arguments: HeroArguments) -> Character { - context.getHero(of: arguments.episode) - } + public var hero: (StarWarsContext, HeroArguments) async throws -> Character - public struct HumanArguments : Codable { + public struct HumanArguments: Codable { public let id: String } - public func human(context: StarWarsContext, arguments: HumanArguments) -> Human? { - context.getHuman(id: arguments.id) - } + public var human: (StarWarsContext, HumanArguments) async throws -> Human? - public struct DroidArguments : Codable { + public struct DroidArguments: Codable { public let id: String } - public func droid(context: StarWarsContext, arguments: DroidArguments) -> Droid? { - context.getDroid(id: arguments.id) - } + public var droid: (StarWarsContext, DroidArguments) async throws -> Droid? - public struct SearchArguments : Codable { + public struct SearchArguments: Codable { public let query: String } - public func search(context: StarWarsContext, arguments: SearchArguments) -> [SearchResult] { - context.search(query: arguments.query) - } + public var search: (StarWarsContext, SearchArguments) async throws -> [SearchResult] +} + +@available(macOS 12.0.0, *) +public extension StarWarsResolver { + static let live = StarWarsResolver( + hero: { context, arguments in + await context.getHero(of: arguments.episode) + }, + human: { context, arguments in + await context.getHuman(id: arguments.id) + }, + droid: { context, arguments in + await context.getDroid(id: arguments.id) + }, + search: { context, arguments in + await context.search(query: arguments.query) + } + ) } diff --git a/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift b/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift index 4df2ddf..0603c3a 100644 --- a/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift +++ b/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift @@ -4,6 +4,7 @@ import GraphQL @testable import Graphiti +@available(macOS 12, *) class StarWarsIntrospectionTests : XCTestCase { private let api = StarWarsAPI() private let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) @@ -13,13 +14,15 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionTypeQuery() throws { - let query = "query IntrospectionTypeQuery {" + - " __schema {" + - " types {" + - " name" + - " }" + - " }" + - "}" + let query = """ + query IntrospectionTypeQuery { + __schema { + types { + name + } + } + } + """ let expected = GraphQLResult( data: [ @@ -86,9 +89,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -99,13 +103,15 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionQueryTypeQuery() throws { - let query = "query IntrospectionQueryTypeQuery {" + - " __schema {" + - " queryType {" + - " name" + - " }" + - " }" + - "}" + let query = """ + query IntrospectionQueryTypeQuery { + __schema { + queryType { + name + } + } + } + """ let expected = GraphQLResult( data: [ @@ -119,9 +125,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -132,11 +139,13 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionDroidTypeQuery() throws { - let query = "query IntrospectionDroidTypeQuery {" + - " __type(name: \"Droid\") {" + - " name" + - " }" + - "}" + let query = """ + query IntrospectionDroidTypeQuery { + __type(name: "Droid") { + name + } + } + """ let expected = GraphQLResult( data: [ @@ -148,9 +157,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -161,12 +171,14 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionDroidKindQuery() throws { - let query = "query IntrospectionDroidKindQuery {" + - " __type(name: \"Droid\") {" + - " name" + - " kind" + - " }" + - "}" + let query = """ + query IntrospectionDroidKindQuery { + __type(name: "Droid") { + name + kind + } + } + """ let expected = GraphQLResult( data: [ @@ -179,9 +191,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -192,12 +205,14 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionCharacterKindQuery() throws { - let query = "query IntrospectionCharacterKindQuery {" + - " __type(name: \"Character\") {" + - " name" + - " kind" + - " }" + - "}" + let query = """ + query IntrospectionCharacterKindQuery { + __type(name: \"Character\") { + name + kind + } + } + """ let expected = GraphQLResult( data: [ @@ -210,9 +225,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -223,18 +239,20 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionDroidFieldsQuery() throws { - let query = "query IntrospectionDroidFieldsQuery {" + - " __type(name: \"Droid\") {" + - " name" + - " fields {" + - " name" + - " type {" + - " name" + - " kind" + - " }" + - " }" + - " }" + - "}" + let query = """ + query IntrospectionDroidFieldsQuery { + __type(name: "Droid") { + name + fields { + name + type { + name + kind + } + } + } + } + """ let expected = GraphQLResult( data: [ @@ -290,9 +308,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -303,22 +322,24 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionDroidNestedFieldsQuery() throws { - let query = "query IntrospectionDroidNestedFieldsQuery {" + - " __type(name: \"Droid\") {" + - " name" + - " fields {" + - " name" + - " type {" + - " name" + - " kind" + - " ofType {" + - " name" + - " kind" + - " }" + - " }" + - " }" + - " }" + - "}" + let query = """ + query IntrospectionDroidNestedFieldsQuery { + __type(name: "Droid") { + name + fields { + name + type { + name + kind + ofType { + name + kind + } + } + } + } + } + """ let expected = GraphQLResult( data: [ @@ -395,9 +416,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -408,28 +430,30 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionFieldArgsQuery() throws { - let query = "query IntrospectionFieldArgsQuery {" + - " __schema {" + - " queryType {" + - " fields {" + - " name" + - " args {" + - " name" + - " description" + - " type {" + - " name" + - " kind" + - " ofType {" + - " name" + - " kind" + - " }" + - " }" + - " defaultValue" + - " }" + - " }" + - " }" + - " }" + - "}" + let query = """ + query IntrospectionFieldArgsQuery { + __schema { + queryType { + fields { + name + args { + name + description + type { + name + kind + ofType { + name + kind + } + } + defaultValue + } + } + } + } + } + """ let expected = GraphQLResult( data: [ @@ -513,9 +537,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -526,12 +551,14 @@ class StarWarsIntrospectionTests : XCTestCase { } func testIntrospectionDroidDescriptionQuery() throws { - let query = "query IntrospectionDroidDescriptionQuery {" + - " __type(name: \"Droid\") {" + - " name" + - " description" + - " }" + - "}" + let query = """ + query IntrospectionDroidDescriptionQuery { + __type(name: "Droid") { + name + description + } + } + """ let expected = GraphQLResult( data: [ @@ -544,9 +571,10 @@ class StarWarsIntrospectionTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -557,6 +585,7 @@ class StarWarsIntrospectionTests : XCTestCase { } } +@available(macOS 12, *) extension StarWarsIntrospectionTests { static var allTests: [(String, (StarWarsIntrospectionTests) -> () throws -> Void)] { return [ diff --git a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift index 023067d..accbb90 100644 --- a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift +++ b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift @@ -3,7 +3,7 @@ import NIO @testable import Graphiti import GraphQL -@available(OSX 10.15, *) +@available(macOS 12, *) class StarWarsQueryTests : XCTestCase { private let api = StarWarsAPI() private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) @@ -15,17 +15,18 @@ class StarWarsQueryTests : XCTestCase { func testHeroNameQuery() throws { let query = """ query HeroNameQuery { - hero { - name - } + hero { + name + } } """ let expected = GraphQLResult(data: ["hero": ["name": "R2-D2"]]) let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, + resolver: .live, context: StarWarsContext(), on: group ).whenSuccess { result in @@ -39,13 +40,13 @@ class StarWarsQueryTests : XCTestCase { func testHeroNameAndFriendsQuery() throws { let query = """ query HeroNameAndFriendsQuery { - hero { - id - name - friends { - name - } + hero { + id + name + friends { + name } + } } """ @@ -65,9 +66,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -80,16 +82,16 @@ class StarWarsQueryTests : XCTestCase { func testNestedQuery() throws { let query = """ query NestedQuery { - hero { - name - friends { - name - appearsIn - friends { - name - } - } - } + hero { + name + friends { + name + appearsIn + friends { + name + } + } + } } """ @@ -134,9 +136,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -149,9 +152,9 @@ class StarWarsQueryTests : XCTestCase { func testFetchLukeQuery() throws { let query = """ query FetchLukeQuery { - human(id: "1000") { - name - } + human(id: "1000") { + name + } } """ @@ -165,9 +168,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -180,9 +184,9 @@ class StarWarsQueryTests : XCTestCase { func testFetchSomeIDQuery() throws { let query = """ query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } + human(id: $someId) { + name + } } """ @@ -202,9 +206,10 @@ class StarWarsQueryTests : XCTestCase { expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group, variables: params ).whenSuccess { result in @@ -226,9 +231,10 @@ class StarWarsQueryTests : XCTestCase { expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group, variables: params ).whenSuccess { result in @@ -248,9 +254,10 @@ class StarWarsQueryTests : XCTestCase { expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group, variables: params ).whenSuccess { result in @@ -264,9 +271,9 @@ class StarWarsQueryTests : XCTestCase { func testFetchLukeAliasedQuery() throws { let query = """ query FetchLukeAliasedQuery { - luke: human(id: "1000") { - name - } + luke: human(id: "1000") { + name + } } """ @@ -280,9 +287,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -295,12 +303,12 @@ class StarWarsQueryTests : XCTestCase { func testFetchLukeAndLeiaAliasedQuery() throws { let query = """ query FetchLukeAndLeiaAliasedQuery { - luke: human(id: "1000") { - name - } - leia: human(id: "1003") { - name - } + luke: human(id: "1000") { + name + } + leia: human(id: "1003") { + name + } } """ @@ -317,9 +325,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -332,14 +341,14 @@ class StarWarsQueryTests : XCTestCase { func testDuplicateFieldsQuery() throws { let query = """ query DuplicateFieldsQuery { - luke: human(id: "1000") { - name - homePlanet { name } - } - leia: human(id: "1003") { - name - homePlanet { name } - } + luke: human(id: "1000") { + name + homePlanet { name } + } + leia: human(id: "1003") { + name + homePlanet { name } + } } """ @@ -358,9 +367,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -373,16 +383,16 @@ class StarWarsQueryTests : XCTestCase { func testUseFragmentQuery() throws { let query = """ query UseFragmentQuery { - luke: human(id: "1000") { - ...HumanFragment - } - leia: human(id: "1003") { - ...HumanFragment - } + luke: human(id: "1000") { + ...HumanFragment + } + leia: human(id: "1003") { + ...HumanFragment + } } fragment HumanFragment on Human { - name - homePlanet { name } + name + homePlanet { name } } """ @@ -401,9 +411,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -416,10 +427,10 @@ class StarWarsQueryTests : XCTestCase { func testCheckTypeOfR2Query() throws { let query = """ query CheckTypeOfR2Query { - hero { - __typename - name - } + hero { + __typename + name + } } """ @@ -434,9 +445,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -449,10 +461,10 @@ class StarWarsQueryTests : XCTestCase { func testCheckTypeOfLukeQuery() throws { let query = """ query CheckTypeOfLukeQuery { - hero(episode: EMPIRE) { - __typename - name - } + hero(episode: EMPIRE) { + __typename + name + } } """ @@ -467,9 +479,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -482,10 +495,10 @@ class StarWarsQueryTests : XCTestCase { func testSecretBackstoryQuery() throws { let query = """ query SecretBackstoryQuery { - hero { - name - secretBackstory - } + hero { + name + secretBackstory + } } """ @@ -507,9 +520,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -522,13 +536,13 @@ class StarWarsQueryTests : XCTestCase { func testSecretBackstoryListQuery() throws { let query = """ query SecretBackstoryListQuery { - hero { - name - friends { - name - secretBackstory - } + hero { + name + friends { + name + secretBackstory } + } } """ @@ -573,9 +587,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -588,10 +603,10 @@ class StarWarsQueryTests : XCTestCase { func testSecretBackstoryAliasQuery() throws { let query = """ query SecretBackstoryAliasQuery { - mainHero: hero { - name - story: secretBackstory - } + mainHero: hero { + name + story: secretBackstory + } } """ @@ -613,9 +628,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -626,57 +642,61 @@ class StarWarsQueryTests : XCTestCase { } func testNonNullableFieldsQuery() throws { - struct A : Codable { - func nullableA(context: NoContext, arguments: NoArguments) -> A? { - return A() + struct A: Codable { + var nullableA: (Void, Void) async throws -> A? { + return { _, _ in + A() + } } - func nonNullA(context: NoContext, arguments: NoArguments) -> A { - return A() + var nonNullA: (Void, Void) async throws -> A { + return { _, _ in + A() + } } - func `throws`(context: NoContext, arguments: NoArguments) throws -> String { - struct 🏃 : Error, CustomStringConvertible { - let description: String - } + var `throws`: (Void, Void) async throws -> String { + return { _, _ in + struct 🏃: Error, CustomStringConvertible { + let description: String + } - throw 🏃(description: "catch me if you can.") + throw 🏃(description: "catch me if you can.") + } } } struct TestResolver { - func nullableA(context: NoContext, arguments: NoArguments) -> A? { - return A() + var nullableA: (Void, Void) async throws -> A? = { _, _ in + A() } } - struct MyAPI : API { - var resolver: TestResolver = TestResolver() - - let schema = try! Schema { + struct MyAPI { + let schema = Schema { Type(A.self) { - Field("nullableA", at: A.nullableA, as: (TypeReference?).self) - Field("nonNullA", at: A.nonNullA, as: TypeReference.self) - Field("throws", at: A.throws) + Field("nullableA", at: \.nullableA, as: (TypeReference?).self) + Field("nonNullA", at: \.nonNullA, as: TypeReference.self) + Field("throws", at: \.throws) } Query { - Field("nullableA", at: TestResolver.nullableA) + Field("nullableA", at: \.nullableA) } } } let query = """ query { + nullableA { nullableA { - nullableA { - nonNullA { - nonNullA { - throws - } - } + nonNullA { + nonNullA { + throws } + } } + } } """ @@ -698,8 +718,9 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() let api = MyAPI() - api.execute( + api.schema.execute( request: query, + resolver: TestResolver(), context: NoContext(), on: group ).whenSuccess { result in @@ -713,19 +734,19 @@ class StarWarsQueryTests : XCTestCase { func testSearchQuery() throws { let query = """ query { - search(query: "o") { - ... on Planet { - name - diameter - } - ... on Human { - name - } - ... on Droid { - name - primaryFunction - } + search(query: "o") { + ... on Planet { + name + diameter + } + ... on Human { + name } + ... on Droid { + name + primaryFunction + } + } } """ @@ -742,9 +763,10 @@ class StarWarsQueryTests : XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -761,13 +783,13 @@ class StarWarsQueryTests : XCTestCase { query = """ query Hero { - hero { - name + hero { + name - friends @include(if: false) { - name - } + friends @include(if: false) { + name } + } } """ @@ -781,9 +803,10 @@ class StarWarsQueryTests : XCTestCase { expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -794,13 +817,13 @@ class StarWarsQueryTests : XCTestCase { query = """ query Hero { - hero { - name + hero { + name - friends @include(if: true) { - name - } + friends @include(if: true) { + name } + } } """ @@ -819,9 +842,10 @@ class StarWarsQueryTests : XCTestCase { expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: StarWarsContext(), + resolver: .live, + context: .live, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -832,7 +856,7 @@ class StarWarsQueryTests : XCTestCase { } } -@available(OSX 10.15, *) +@available(macOS 12, *) extension StarWarsQueryTests { static var allTests: [(String, (StarWarsQueryTests) -> () throws -> Void)] { return [ diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 8b775ae..4a1fde4 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,7 +2,9 @@ import XCTest @testable import GraphitiTests XCTMain([ + testCase(CounterTests.allTests), testCase(HelloWorldTests.allTests), testCase(StarWarsQueryTests.allTests), testCase(StarWarsIntrospectionTests.allTests), + testCase(ScalarTests.allTests), ])