From cbd8f48cf68600bd5a7b0e1e817c321683bba05c Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:57:11 -0300 Subject: [PATCH 01/19] feature: Start async await support --- Package.resolved | 17 +-- Package.swift | 5 +- Sources/Graphiti/Argument/NoArguments.swift | 2 +- Sources/Graphiti/Connection/Connection.swift | 6 +- Sources/Graphiti/Field/Field/Field.swift | 127 +++++++++++++++- Sources/Graphiti/Schema/Schema.swift | 10 +- .../CounterTests/CounterTests.swift | 138 +++++++++++++++++ .../HelloWorldTests/HelloWorldTests.swift | 143 ++++++++---------- .../StarWarsAPI/StarWarsAPI.swift | 2 +- Tests/LinuxMain.swift | 2 + 10 files changed, 340 insertions(+), 112 deletions(-) create mode 100644 Tests/GraphitiTests/CounterTests/CounterTests.swift 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..14413a6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.5 import PackageDescription let package = Package( @@ -7,7 +7,8 @@ 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(path: "../GraphQL") ], targets: [ .target(name: "Graphiti", dependencies: ["GraphQL"]), diff --git a/Sources/Graphiti/Argument/NoArguments.swift b/Sources/Graphiti/Argument/NoArguments.swift index 05cc88e..4720d38 100644 --- a/Sources/Graphiti/Argument/NoArguments.swift +++ b/Sources/Graphiti/Argument/NoArguments.swift @@ -1,3 +1,3 @@ -public struct NoArguments : Decodable { +public struct NoArguments: Decodable { init() {} } 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/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 8745af0..6f77b71 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -98,6 +98,7 @@ public class Field : Fiel // 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 +107,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 +118,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 +128,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 +142,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 +151,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 +162,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 +172,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 +186,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 +194,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 +206,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 +216,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 +227,116 @@ 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 where FieldType: Encodable { + 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, FieldType: Encodable { + 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 + ) where ResolveType: Encodable { + 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, at keyPath: KeyPath ) { - let syncResolve: SyncResolve = { type in - { context, _ in + let syncResolve: SyncResolve = { type in + { _, _ in type[keyPath: keyPath] } } @@ -237,10 +350,10 @@ public extension Field where Arguments == NoArguments { _ 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] } } diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index 256f9bf..cc69185 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -4,7 +4,7 @@ import NIO public final class Schema { public let schema: GraphQLSchema - private init( + internal init( coders: Coders, components: [Component] ) throws { @@ -32,8 +32,8 @@ public extension Schema { convenience init( coders: Coders = Coders(), @ComponentBuilder _ components: () -> Component - ) throws { - try self.init( + ) { + try! self.init( coders: coders, components: [components()] ) @@ -42,8 +42,8 @@ public extension Schema { convenience init( coders: Coders = Coders(), @ComponentBuilder _ components: () -> [Component] - ) throws { - try self.init( + ) { + try! self.init( coders: coders, components: components() ) diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift new file mode 100644 index 0000000..4d172af --- /dev/null +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -0,0 +1,138 @@ +import XCTest +import GraphQL +import NIO +@testable import Graphiti + +actor CounterContext { + var count = 0 + + func increment() -> Int { + count += 1 + return count + } + + func decrement() -> Int { + count -= 1 + return count + } +} + +struct CounterResolver { + var count: Resolve + var increment: Resolve + var decrement: Resolve +} + +@available(macOS 12, *) +struct CounterAPI: API { + let resolver: CounterResolver + + let schema = Schema { + Query { + Field("count", at: \.count) + } + + Mutation { + Field("increment", at: \.increment) + Field("decrement", at: \.decrement) + } + } +} + +extension CounterResolver { + static let test = CounterResolver( + count: { context, _ in + await context.count + }, + increment: { context, _ in + await context.increment() + }, + decrement: { context, _ in + await context.decrement() + } + ) +} + +@available(macOS 12, *) +class CounterTests: XCTestCase { + private let api = CounterAPI(resolver: .test) + private let context = CounterContext() + private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + + deinit { + try? self.group.syncShutdownGracefully() + } + + func testCounter() throws { + var query = "{ count }" + var expected = GraphQLResult(data: ["count": 0]) + var expectation = XCTestExpectation() + + api.execute( + request: query, + context: context, + on: group + ).whenSuccess { result in + XCTAssertEqual(result, expected) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10) + + query = """ + mutation { + increment + } + """ + + expected = GraphQLResult( + data: ["increment": 1] + ) + + expectation = XCTestExpectation() + + api.execute( + request: query, + context: context, + on: group + ).whenSuccess { result in + XCTAssertEqual(result, expected) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10) + + query = """ + mutation { + decrement + } + """ + + expected = GraphQLResult( + data: ["decrement": 0] + ) + + expectation = XCTestExpectation() + + api.execute( + request: query, + context: context, + on: group + ).whenSuccess { result in + XCTAssertEqual(result, expected) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10) + } +} + +@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..d4e93b9 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,12 @@ struct ID : Codable { } } -struct User : Codable { - let id: String +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,8 +35,9 @@ 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 } @@ -47,67 +48,48 @@ struct User : Codable { } } -struct UserInput : Codable { - let id: String +struct UserInput: Codable { + let id: ID let name: String? let friends: [UserInput]? } -struct UserEvent : Codable { +struct UserEvent: Codable { let user: User } final class HelloContext { - func hello() -> String { + func hello() async -> String { "world" } } struct HelloResolver { - func hello(context: HelloContext, arguments: NoArguments) -> String { - context.hello() - } + var hello: Resolve - func asyncHello( - context: HelloContext, - arguments: NoArguments, - group: EventLoopGroup - ) -> EventLoopFuture { - group.next().makeSucceededFuture(context.hello()) - } - - 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 - } + var getId: Resolve + var getUser: Resolve - func getUser(context: HelloContext, arguments: NoArguments) -> User { - User(id: "123", name: "John Doe", friends: nil) - } - - 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() +@available(macOS 12, *) +struct HelloAPI: API { + let resolver: HelloResolver let schema = try! Schema { Scalar(Float.self) @@ -133,30 +115,51 @@ 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 { - private let api = HelloAPI() +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(resolver: .test) + private let context = HelloContext() private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) deinit { @@ -166,30 +169,11 @@ class HelloWorldTests : XCTestCase { func testHello() throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) - let expectation = XCTestExpectation() api.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, + context: context, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -215,7 +199,7 @@ class HelloWorldTests : XCTestCase { api.execute( request: query, - context: api.context, + context: context, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -239,7 +223,7 @@ class HelloWorldTests : XCTestCase { api.execute( request: query, - context: api.context, + context: context, on: group, variables: ["float": 4] ).whenSuccess { result in @@ -259,7 +243,7 @@ class HelloWorldTests : XCTestCase { api.execute( request: query, - context: api.context, + context: context, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -275,12 +259,11 @@ class HelloWorldTests : XCTestCase { """ expected = GraphQLResult(data: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"]) - let expectationC = XCTestExpectation() api.execute( request: query, - context: api.context, + context: context, on: group, variables: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"] ).whenSuccess { result in @@ -300,7 +283,7 @@ class HelloWorldTests : XCTestCase { api.execute( request: query, - context: api.context, + context: context, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -320,17 +303,17 @@ 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( request: mutation, - context: api.context, + context: context, on: group, variables: variables ).whenSuccess { result in @@ -355,17 +338,17 @@ 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( request: mutation, - context: api.context, + context: context, on: group, variables: variables ).whenSuccess { result in @@ -377,11 +360,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/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index c33f558..db5bbc9 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -1,6 +1,6 @@ import Graphiti -public struct StarWarsAPI : API { +public struct StarWarsAPI: API { public let resolver = StarWarsResolver() public let schema = try! Schema { 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), ]) From 1091954a1577f121f07d83a251a5ddbdca4c4ae2 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 12:19:05 -0300 Subject: [PATCH 02/19] feature: Update README.md --- README.md | 150 ++++++++++----- Sources/Graphiti/API/API.swift | 8 +- Sources/Graphiti/Field/Field/Field.swift | 8 +- Sources/Graphiti/Schema/Schema.swift | 42 ++++- .../CounterTests/CounterTests.swift | 177 +++++++++++------- .../HelloWorldTests/HelloWorldTests.swift | 6 +- Tests/GraphitiTests/ScalarTests.swift | 24 +-- .../StarWarsAPI/StarWarsAPI.swift | 2 +- .../StarWarsTests/StarWarsQueryTests.swift | 2 +- 9 files changed, 286 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index a87e01b..af081f7 100644 --- a/README.md +++ b/README.md @@ -23,91 +23,156 @@ through that README and the corresponding tests in parallel. ### Using Graphiti -Add Graphiti to your `Package.swift` - -```swift -import PackageDescription - -let package = Package( - dependencies: [ - .Package(url: "https://github.com/GraphQLSwift/Graphiti.git", .upToNextMinor(from: "0.20.1")), - ] -) -``` - -Graphiti provides two important capabilities: building a type schema, and +Add Graphiti to your `Package.swift`. Graphiti provides two important capabilities: building a type schema, and serving queries against that type schema. #### Defining entities -First, we declare our regular Swift entities. +First, we declare 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 Message : Codable { - let content: String +struct Counter: Encodable { + var count: Int } ``` -⭐️ 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. +⭐️ Notice that this step does not require importing `Graphiti`. One of the main design decisions behind Graphiti is **not** to pollute your entities declarations. This way you can bring your entities to any other environment 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. +Second step is to create your API's **context** actor. 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 derives your entities from a database or another service. ```swift -struct Context { - func message() -> Message { - Message(content: "Hello, world!") +actor CounterContext { + var counter: Counter + + init(counter: Counter) { + self.counter = counter + } + + func increment() -> Counter { + counter.count += 1 + return counter + } + + func decrement() -> Counter { + counter.count -= 1 + return counter + } + + func increment(by count: Int) -> Counter { + counter.count += count + return counter + } + + func decrement(by count: Int) -> Counter { + counter.count -= count + return counter } } ``` -⭐️ Notice again that this step doesn't require Graphiti. It's purely business logic. +⭐️ Notice that this step does not require importing `Graphiti`. It is purely your API's business logic. #### Defining the GraphQL API resolver -Now that we have our entities and context we can create the GraphQL API resolver. +Now that we have our entities and context we can declare the GraphQL API resolver. These 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 counter: (CounterContext, Void) async throws -> Counter + var increment: (CounterContext, Void) async throws -> Counter + var decrement: (CounterContext, Void) async throws -> Counter + + struct IncrementByArguments: Decodable { + let count: Int } + + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Counter + + struct DecrementByArguments: Decodable { + let count: Int + } + + var decrementBy: (CounterContext, DecrementByArguments) async throws -> Counter } ``` +⭐️ Notice that this step does not require importing `Graphiti`. However, all resolver functions must take the following shape: + +```swift +(Context, Arguments) async thows -> Output where Arguments: Decodable, Output: Encodable +``` + +In case your resolve function does not use any arguments you can use the following shape: + + +```swift +(Context, Void) async thows -> Output where Output: Encodable +``` + #### Defining the GraphQL API schema Now we can finally 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(Counter.self) { + Field("count", at: \.count) + } + + Query { + Field("counter", at: \.counter) + } - Query { - Field("message", at: Resolver.message) + Mutation { + Field("increment", at: \.increment) + Field("decrement", at: \.decrement) + + Field("incrementBy", at: \.incrementBy) { + Argument("count", at: \.count) + } + + Field("decrementBy", at: \.decrementBy) { + Argument("count", at: \.count) } } } } ``` -⭐️ Notice that `API` allows dependency injection. You could pass mocks of `resolver` and `context` when testing, for example. +⭐️ Now we finally import Graphiti. Notice that `Schema` allows dependency injection. You could pass mocks of `resolver` and `context` to `Schema.execute` when testing, for example. #### 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 resolver: + +```swift +extension CounterResolver { + static let live = CounterResolver( + counter: { context, _ in + await context.counter + }, + increment: { context, _ in + await context.increment() + }, + decrement: { context, _ in + await context.decrement() + }, + incrementBy: { context, arguments in + await context.increment(by: arguments.count) + }, + decrementBy: { context, arguments in + await context.decrement(by: arguments.count) + } + ) +} +``` + +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 your resolver when testing. ```swift import NIO @@ -187,3 +252,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..f0ddf43 100644 --- a/Sources/Graphiti/API/API.swift +++ b/Sources/Graphiti/API/API.swift @@ -16,11 +16,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 +33,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/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 6f77b71..671a41b 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -1,7 +1,7 @@ import GraphQL import NIO -public class Field : FieldComponent { +public class Field: FieldComponent where Arguments: Decodable { let name: String let arguments: [ArgumentComponent] let resolve: AsyncResolve @@ -234,14 +234,14 @@ public extension Field { public typealias Resolve = ( _ context: Context, _ arguments: Arguments -) async throws -> ResolveType +) async throws -> ResolveType where ResolveType: Encodable @available(macOS 12, *) public extension Field where FieldType: Encodable { convenience init( _ name: String, at keyPath: KeyPath>, - @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = {[]} + @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] ) { let asyncResolve: AsyncResolve = { type in { context, arguments, group in @@ -265,7 +265,7 @@ public extension Field { _ name: String, at keyPath: KeyPath>, as: FieldType.Type, - @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = {[]} + @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] ) where ResolveType: Encodable { let asyncResolve: AsyncResolve = { type in { context, arguments, group in diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index cc69185..1d0e055 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -48,7 +48,8 @@ public extension Schema { components: components() ) } - + + @available(*, deprecated, message: "Use the signature where the label for eventLoopGroup is `on`.") func execute( request: String, resolver: Resolver, @@ -56,6 +57,24 @@ public extension Schema { eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil + ) -> EventLoopFuture { + self.execute( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ) + } + + func execute( + request: String, + resolver: Resolver, + context: Context, + on eventLoopGroup: EventLoopGroup, + variables: [String: Map] = [:], + operationName: String? = nil ) -> EventLoopFuture { do { return try graphql( @@ -71,7 +90,8 @@ public extension Schema { return eventLoopGroup.next().makeFailedFuture(error) } } - + + @available(*, deprecated, message: "Use the signature where the label for eventLoopGroup is `on`.") func subscribe( request: String, resolver: Resolver, @@ -79,6 +99,24 @@ public extension Schema { eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil + ) -> EventLoopFuture { + self.subscribe( + request: request, + resolver: resolver, + context: context, + on: eventLoopGroup, + variables: variables, + operationName: operationName + ) + } + + 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/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 4d172af..7165b7f 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -3,60 +3,108 @@ import GraphQL import NIO @testable import Graphiti +struct Counter: Encodable { + var count: Int +} + actor CounterContext { - var count = 0 + var counter: Counter + + init(counter: Counter) { + self.counter = counter + } + + func increment() -> Counter { + counter.count += 1 + return counter + } + + func decrement() -> Counter { + counter.count -= 1 + return counter + } - func increment() -> Int { - count += 1 - return count + func increment(by count: Int) -> Counter { + counter.count += count + return counter } - func decrement() -> Int { - count -= 1 - return count + func decrement(by count: Int) -> Counter { + counter.count -= count + return counter } } struct CounterResolver { - var count: Resolve - var increment: Resolve - var decrement: Resolve + var counter: (CounterContext, Void) async throws -> Counter + var increment: (CounterContext, Void) async throws -> Counter + var decrement: (CounterContext, Void) async throws -> Counter + + struct IncrementByArguments: Decodable { + let count: Int + } + + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Counter + + struct DecrementByArguments: Decodable { + let count: Int + } + + var decrementBy: (CounterContext, DecrementByArguments) async throws -> Counter } @available(macOS 12, *) -struct CounterAPI: API { - let resolver: CounterResolver - +struct CounterAPI { let schema = Schema { - Query { + Type(Counter.self) { Field("count", at: \.count) } + + Query { + Field("counter", at: \.counter) + } Mutation { Field("increment", at: \.increment) Field("decrement", at: \.decrement) + + Field("incrementBy", at: \.incrementBy) { + Argument("count", at: \.count) + } + + Field("decrementBy", at: \.decrementBy) { + Argument("count", at: \.count) + } } } } +extension CounterContext { + static let live = CounterContext(counter: Counter(count: 0)) +} + extension CounterResolver { - static let test = CounterResolver( - count: { context, _ in - await context.count + static let live = CounterResolver( + counter: { context, _ in + await context.counter }, increment: { context, _ in await context.increment() }, decrement: { context, _ in await context.decrement() + }, + incrementBy: { context, arguments in + await context.increment(by: arguments.count) + }, + decrementBy: { context, arguments in + await context.decrement(by: arguments.count) } ) } @available(macOS 12, *) class CounterTests: XCTestCase { - private let api = CounterAPI(resolver: .test) - private let context = CounterContext() private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) deinit { @@ -64,66 +112,67 @@ class CounterTests: XCTestCase { } func testCounter() throws { - var query = "{ count }" - var expected = GraphQLResult(data: ["count": 0]) - var expectation = XCTestExpectation() + let api = CounterAPI() - api.execute( + var query = "query { counter { count } }" + var expected = GraphQLResult(data: ["counter": ["count": 0]]) + + var result = try api.schema.execute( request: query, - context: context, + resolver: .live, + context: .live, on: group - ).whenSuccess { result in - XCTAssertEqual(result, expected) - expectation.fulfill() - } + ).wait() - wait(for: [expectation], timeout: 10) + XCTAssertEqual(result, expected) - query = """ - mutation { - increment - } - """ - - expected = GraphQLResult( - data: ["increment": 1] - ) + query = "mutation { increment { count } }" + expected = GraphQLResult(data: ["increment": ["count": 1]]) - expectation = XCTestExpectation() - - api.execute( + result = try api.schema.execute( request: query, - context: context, + resolver: .live, + context: .live, on: group - ).whenSuccess { result in - XCTAssertEqual(result, expected) - expectation.fulfill() - } + ).wait() - wait(for: [expectation], timeout: 10) + XCTAssertEqual(result, expected) - query = """ - mutation { - decrement - } - """ - - expected = GraphQLResult( - data: ["decrement": 0] - ) + query = "mutation { decrement { count } }" + expected = GraphQLResult(data: ["decrement": ["count": 0]]) - expectation = XCTestExpectation() + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + XCTAssertEqual(result, expected) + + query = "mutation { incrementBy(count: 5) { count } }" + expected = GraphQLResult(data: ["incrementBy": ["count": 5]]) - api.execute( + result = try api.schema.execute( request: query, - context: context, + resolver: .live, + context: .live, on: group - ).whenSuccess { result in - XCTAssertEqual(result, expected) - expectation.fulfill() - } + ).wait() + + XCTAssertEqual(result, expected) + + query = "mutation { decrementBy(count: 5) { count } }" + expected = GraphQLResult(data: ["decrementBy": ["count": 0]]) - wait(for: [expectation], timeout: 10) + result = try api.schema.execute( + request: query, + resolver: .live, + context: .live, + on: group + ).wait() + + XCTAssertEqual(result, expected) } } diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift index d4e93b9..f948b93 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift @@ -65,7 +65,7 @@ final class HelloContext { } struct HelloResolver { - var hello: Resolve + var hello: Resolve struct FloatArguments: Codable { let float: Float @@ -78,7 +78,7 @@ struct HelloResolver { } var getId: Resolve - var getUser: Resolve + var getUser: Resolve struct AddUserArguments: Codable { let user: UserInput @@ -91,7 +91,7 @@ struct HelloResolver { struct HelloAPI: API { let resolver: HelloResolver - let schema = try! Schema { + 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).") diff --git a/Tests/GraphitiTests/ScalarTests.swift b/Tests/GraphitiTests/ScalarTests.swift index e291e38..29c8c89 100644 --- a/Tests/GraphitiTests/ScalarTests.swift +++ b/Tests/GraphitiTests/ScalarTests.swift @@ -17,7 +17,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(UUID.self, as: "UUID") Type(UUIDOutput.self) { Field("value", at: \.value) @@ -69,7 +69,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(UUID.self, as: "UUID") Type(UUIDOutput.self) { Field("value", at: \.value) @@ -127,7 +127,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(UUID.self, as: "UUID") Type(UUIDOutput.self) { Field("value", at: \.value) @@ -184,7 +184,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") @@ -241,7 +241,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 +304,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 +360,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 +412,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 +470,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 +524,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 +580,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 +642,7 @@ class ScalarTests : XCTestCase { } } - let testSchema = try Schema { + let testSchema = Schema { Scalar(DictCodedCoordinate.self, as: "Coordinate") Type(CoordinateOutput.self) { Field("value", at: \.value) diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index db5bbc9..5a24104 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -3,7 +3,7 @@ import Graphiti public struct StarWarsAPI: API { public let resolver = StarWarsResolver() - public let schema = try! Schema { + public let schema = Schema { Enum(Episode.self) { Value(.newHope) .description("Released in 1977.") diff --git a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift index 023067d..04ed574 100644 --- a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift +++ b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift @@ -653,7 +653,7 @@ class StarWarsQueryTests : XCTestCase { struct MyAPI : API { var resolver: TestResolver = TestResolver() - let schema = try! Schema { + let schema = Schema { Type(A.self) { Field("nullableA", at: A.nullableA, as: (TypeReference?).self) Field("nonNullA", at: A.nonNullA, as: TypeReference.self) From 24b8cab05c4ce36aaf554fc3eb5f71948c30fdf3 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 12:57:41 -0300 Subject: [PATCH 03/19] fix: Fix sdk checks --- Sources/Graphiti/Field/Field/Field.swift | 10 +++++----- Tests/GraphitiTests/CounterTests/CounterTests.swift | 3 +++ .../HelloWorldTests/HelloWorldTests.swift | 11 ++++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 6f77b71..af3c537 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -227,15 +227,15 @@ public extension Field { } } +public typealias Resolve = ( + _ context: Context, + _ arguments: Arguments +) async throws -> ResolveType + #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 where FieldType: Encodable { convenience init( diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 4d172af..05f6805 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -3,6 +3,7 @@ import GraphQL import NIO @testable import Graphiti +@available(macOS 12, *) actor CounterContext { var count = 0 @@ -17,6 +18,7 @@ actor CounterContext { } } +@available(macOS 12, *) struct CounterResolver { var count: Resolve var increment: Resolve @@ -39,6 +41,7 @@ struct CounterAPI: API { } } +@available(macOS 12, *) extension CounterResolver { static let test = CounterResolver( count: { context, _ in diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift index d4e93b9..fde2e9a 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift @@ -21,6 +21,7 @@ struct ID: Codable { } } +@available(macOS 12, *) struct User: Codable { let id: ID let name: String? @@ -42,10 +43,6 @@ struct User: Codable { self.friends = nil } } - - func toEvent(context: HelloContext, arguments: NoArguments) throws -> UserEvent { - return UserEvent(user: self) - } } struct UserInput: Codable { @@ -54,18 +51,21 @@ struct UserInput: Codable { let friends: [UserInput]? } +@available(macOS 12, *) struct UserEvent: Codable { let user: User } +@available(macOS 12, *) final class HelloContext { func hello() async -> String { "world" } } +@available(macOS 12, *) struct HelloResolver { - var hello: Resolve + var hello: Resolve struct FloatArguments: Codable { let float: Float @@ -136,6 +136,7 @@ struct HelloAPI: API { } } +@available(macOS 12, *) extension HelloResolver { static let test = HelloResolver( hello: { context, _ in From bb8a9ff8ede6c7cc1143ec4200bd7696dd984a70 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 13:40:39 -0300 Subject: [PATCH 04/19] feature: Add async --- Sources/Graphiti/Field/Field/Field.swift | 5 -- Sources/Graphiti/Schema/Schema.swift | 46 +++++++++++++++++-- .../CounterTests/CounterTests.swift | 3 +- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 05c5153..671a41b 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -227,11 +227,6 @@ public extension Field { } } -public typealias Resolve = ( - _ context: Context, - _ arguments: Arguments -) async throws -> ResolveType - #if compiler(>=5.5) && canImport(_Concurrency) // MARK: Keypath Initializers diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index 1d0e055..de0a432 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -58,7 +58,7 @@ public extension Schema { variables: [String: Map] = [:], operationName: String? = nil ) -> EventLoopFuture { - self.execute( + execute( request: request, resolver: resolver, context: context, @@ -67,7 +67,26 @@ public extension Schema { 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, @@ -100,7 +119,7 @@ public extension Schema { variables: [String: Map] = [:], operationName: String? = nil ) -> EventLoopFuture { - self.subscribe( + subscribe( request: request, resolver: resolver, context: context, @@ -109,7 +128,26 @@ public extension Schema { 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, diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 01d10c3..3e7e1e2 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -8,6 +8,7 @@ struct Counter: Encodable { var count: Int } +@available(macOS 12, *) actor CounterContext { var counter: Counter @@ -86,6 +87,7 @@ extension CounterContext { static let live = CounterContext(counter: Counter(count: 0)) } +@available(macOS 12, *) extension CounterResolver { static let live = CounterResolver( counter: { context, _ in @@ -116,7 +118,6 @@ class CounterTests: XCTestCase { func testCounter() throws { let api = CounterAPI() - var query = "query { counter { count } }" var expected = GraphQLResult(data: ["counter": ["count": 0]]) From 5741a56b0afdae31a3f97690ab7667e6496b9a0d Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 18:53:14 -0300 Subject: [PATCH 05/19] refactor: Start replacing deprecated APIs in tests --- README.md | 176 ++++++++- Sources/Graphiti/API/API.swift | 1 + Sources/Graphiti/Argument/NoArguments.swift | 1 + Sources/Graphiti/Context/NoContext.swift | 1 + Sources/Graphiti/Field/Field/Field.swift | 8 +- Sources/Graphiti/Schema/Schema.swift | 4 +- Sources/Graphiti/Type/Type.swift | 36 +- .../CounterTests/CounterTests.swift | 60 ++- .../HelloWorldTests/HelloWorldTests.swift | 60 +-- .../StarWarsAPI/StarWarsAPI.swift | 25 +- .../StarWarsAPI/StarWarsContext.swift | 14 +- .../StarWarsAPI/StarWarsEntities.swift | 12 +- .../StarWarsAPI/StarWarsResolver.swift | 76 ++-- .../StarWarsIntrospectionTests.swift | 239 ++++++------ .../StarWarsTests/StarWarsQueryTests.swift | 344 ++++++++++-------- 15 files changed, 680 insertions(+), 377 deletions(-) diff --git a/README.md b/README.md index af081f7..a89567c 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,15 @@ struct CounterAPI { #### Querying -To query the schema, we first need to create a live instance of the resolver: +To query the schema, we first need to create a live instance of the context: + +```swift +extension CounterContext { + static let live = CounterContext(counter: Counter(count: 0)) +} +``` + +Now we create a live instance of the resolver: ```swift extension CounterResolver { @@ -172,51 +180,183 @@ extension CounterResolver { } ``` -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 your resolver when testing. +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 { + counter { + count + } +} +""" + +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" : { + "counter" : { + "count" : 0 + } + } +} +``` + +For the increment mutation: + +```swift +let incrementMutation = """ +mutation { + increment { + count + } } +""" + +let incrementResult = try await api.schema.execute( + request: incrementMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(incrementResult) ``` The output will be: ```json -{"data":{"message":{"content":"Hello, world!"}}} +{ + "data" : { + "increment" : { + "count" : 1 + } + } +} ``` -`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 decrement mutation: + +```swift +let decrementMutation = """ +mutation { + decrement { + count + } +} +""" + +let decrementResult = try await api.schema.execute( + request: decrementMutation, + resolver: .live, + context: .live, + on: group +) -#### Async resolvers +debugPrint(decrementResult) +``` -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. +The output will be: + +```json +{ + "data" : { + "decrement" : { + "count" : 0 + } + } +} +``` + +For the incrementBy mutation: ```swift -import NIO +let incrementByMutation = """ +mutation { + incrementBy(count: 5) { + count + } +} +""" -struct Resolver { - func message(context: Context, arguments: NoArguments, group: EventLoopGroup) -> EventLoopFuture { - group.next().makeSucceededFuture(context.message()) +let incrementByResult = try await api.schema.execute( + request: incrementByMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(incrementByResult) +``` + +The output will be: + +```json +{ + "data" : { + "incrementBy" : { + "count" : 5 } + } } ``` +For the decrementBy mutation: + +```swift +let decrementByMutation = """ +mutation { + decrementBy(count: 5) { + count + } +} +""" + +let decrementByResult = try await api.schema.execute( + request: decrementByMutation, + resolver: .live, + context: .live, + on: group +) + +debugPrint(decrementByResult) +``` + +The output will be: + +```json +{ + "data" : { + "decrementBy" : { + "count" : 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 diff --git a/Sources/Graphiti/API/API.swift b/Sources/Graphiti/API/API.swift index f0ddf43..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 diff --git a/Sources/Graphiti/Argument/NoArguments.swift b/Sources/Graphiti/Argument/NoArguments.swift index 4720d38..1d79dd7 100644 --- a/Sources/Graphiti/Argument/NoArguments.swift +++ b/Sources/Graphiti/Argument/NoArguments.swift @@ -1,3 +1,4 @@ +@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/Context/NoContext.swift b/Sources/Graphiti/Context/NoContext.swift index f5d8b04..5493cbe 100644 --- a/Sources/Graphiti/Context/NoContext.swift +++ b/Sources/Graphiti/Context/NoContext.swift @@ -1 +1,2 @@ +@available(*, deprecated, message: "Use the Void directly.") public typealias NoContext = Void diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 671a41b..d953f8c 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -234,10 +234,10 @@ public extension Field { public typealias Resolve = ( _ context: Context, _ arguments: Arguments -) async throws -> ResolveType where ResolveType: Encodable +) async throws -> ResolveType @available(macOS 12, *) -public extension Field where FieldType: Encodable { +public extension Field { convenience init( _ name: String, at keyPath: KeyPath>, @@ -284,7 +284,7 @@ public extension Field { } @available(macOS 12, *) -public extension Field where Arguments == NoArguments, FieldType: Encodable { +public extension Field where Arguments == NoArguments { convenience init( _ name: String, at keyPath: KeyPath> @@ -311,7 +311,7 @@ public extension Field where Arguments == NoArguments { _ name: String, at keyPath: KeyPath>, as: FieldType.Type - ) where ResolveType: Encodable { + ) { let asyncResolve: AsyncResolve = { type in { context, _, group in let promise = group.next().makePromise(of: ResolveType.self) diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index de0a432..101a422 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -49,7 +49,7 @@ public extension Schema { ) } - @available(*, deprecated, message: "Use the signature where the label for eventLoopGroup is `on`.") + @available(*, deprecated, message: "Use the function where the label for the eventLoopGroup parameter is namded `on`.") func execute( request: String, resolver: Resolver, @@ -110,7 +110,7 @@ public extension Schema { } } - @available(*, deprecated, message: "Use the signature where the label for eventLoopGroup is `on`.") + @available(*, deprecated, message: "Use the function where the label for the eventLoopGroup parameter is named `on`.") func subscribe( request: String, resolver: Resolver, diff --git a/Sources/Graphiti/Type/Type.swift b/Sources/Graphiti/Type/Type.swift index d20b93a..5118800 100644 --- a/Sources/Graphiti/Type/Type.swift +++ b/Sources/Graphiti/Type/Type.swift @@ -46,10 +46,11 @@ public final class Type : Component _ fields: () -> FieldComponent ) { self.init( @@ -60,10 +61,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 +76,33 @@ 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() + ) + } +} diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 3e7e1e2..1a741b3 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -108,6 +108,15 @@ extension CounterResolver { ) } +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) @@ -118,7 +127,15 @@ class CounterTests: XCTestCase { func testCounter() throws { let api = CounterAPI() - var query = "query { counter { count } }" + + var query = """ + query { + counter { + count + } + } + """ + var expected = GraphQLResult(data: ["counter": ["count": 0]]) var result = try api.schema.execute( @@ -127,10 +144,17 @@ class CounterTests: XCTestCase { context: .live, on: group ).wait() - + + debugPrint(result) XCTAssertEqual(result, expected) - query = "mutation { increment { count } }" + query = """ + mutation { + increment { + count + } + } + """ expected = GraphQLResult(data: ["increment": ["count": 1]]) result = try api.schema.execute( @@ -139,10 +163,17 @@ class CounterTests: XCTestCase { context: .live, on: group ).wait() - + + debugPrint(result) XCTAssertEqual(result, expected) - query = "mutation { decrement { count } }" + query = """ + mutation { + decrement { + count + } + } + """ expected = GraphQLResult(data: ["decrement": ["count": 0]]) result = try api.schema.execute( @@ -152,9 +183,16 @@ class CounterTests: XCTestCase { on: group ).wait() + debugPrint(result) XCTAssertEqual(result, expected) - query = "mutation { incrementBy(count: 5) { count } }" + query = """ + mutation { + incrementBy(count: 5) { + count + } + } + """ expected = GraphQLResult(data: ["incrementBy": ["count": 5]]) result = try api.schema.execute( @@ -164,9 +202,16 @@ class CounterTests: XCTestCase { on: group ).wait() + debugPrint(result) XCTAssertEqual(result, expected) - query = "mutation { decrementBy(count: 5) { count } }" + query = """ + mutation { + decrementBy(count: 5) { + count + } + } + """ expected = GraphQLResult(data: ["decrementBy": ["count": 0]]) result = try api.schema.execute( @@ -176,6 +221,7 @@ class CounterTests: XCTestCase { on: group ).wait() + debugPrint(result) XCTAssertEqual(result, expected) } } diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift index 25ccb95..b7c6b82 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift @@ -88,9 +88,7 @@ struct HelloResolver { } @available(macOS 12, *) -struct HelloAPI: API { - let resolver: HelloResolver - +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).") @@ -136,6 +134,11 @@ struct HelloAPI: API { } } +@available(macOS 12, *) +extension HelloContext { + static let test = HelloContext() +} + @available(macOS 12, *) extension HelloResolver { static let test = HelloResolver( @@ -159,8 +162,7 @@ extension HelloResolver { @available(macOS 12, *) class HelloWorldTests: XCTestCase { - private let api = HelloAPI(resolver: .test) - private let context = HelloContext() + private let api = HelloAPI() private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) deinit { @@ -172,9 +174,10 @@ class HelloWorldTests: XCTestCase { let expected = GraphQLResult(data: ["hello": "world"]) let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -198,9 +201,10 @@ class HelloWorldTests: XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: query, - context: context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -216,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: context, + resolver: .test, + context: .test, on: group, variables: ["float": 4] ).whenSuccess { result in @@ -236,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: context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -255,16 +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: context, + resolver: .test, + context: .test, on: group, variables: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"] ).whenSuccess { result in @@ -276,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: context, + resolver: .test, + context: .test, on: group ).whenSuccess { result in XCTAssertEqual(result, expected) @@ -312,9 +320,10 @@ class HelloWorldTests: XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: mutation, - context: context, + resolver: .test, + context: .test, on: group, variables: variables ).whenSuccess { result in @@ -347,9 +356,10 @@ class HelloWorldTests: XCTestCase { let expectation = XCTestExpectation() - api.execute( + api.schema.execute( request: mutation, - context: context, + resolver: .test, + context: .test, on: group, variables: variables ).whenSuccess { result in diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index 5a24104..306a984 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -1,8 +1,7 @@ import Graphiti -public struct StarWarsAPI: API { - public let resolver = StarWarsResolver() - +@available(macOS 12, *) +public struct StarWarsAPI { public let schema = Schema { Enum(Episode.self) { Value(.newHope) @@ -44,30 +43,30 @@ public struct StarWarsAPI: API { } .description("A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY.") - Type(Human.self, interfaces: [Character.self]) { + Type(Human.self, implements: Character.self) { Field("id", at: \.id) Field("name", at: \.name) Field("appearsIn", at: \.appearsIn) Field("homePlanet", at: \.homePlanet) - Field("friends", at: Human.getFriends, as: [Character].self) + Field.init("friends", at: \.getFriends) .description("The friends of the human, or an empty list if they have none.") - Field("secretBackstory", at: Human.getSecretBackstory) + Field("secretBackstory", at: \.getSecretBackstory) .description("Where are they from and how they came to be who they are.") } .description("A humanoid creature in the Star Wars universe.") - Type(Droid.self, interfaces: [Character.self]) { + Type(Droid.self, implements: Character.self) { Field("id", at: \.id) Field("name", at: \.name) Field("appearsIn", at: \.appearsIn) Field("primaryFunction", at: \.primaryFunction) - Field("friends", at: Droid.getFriends, as: [Character].self) + Field("friends", at: \.getFriends) .description("The friends of the droid, or an empty list if they have none.") - Field("secretBackstory", at: Droid.getSecretBackstory) + Field("secretBackstory", at: \.getSecretBackstory) .description("Where are they from and how they came to be who they are.") } .description("A mechanical creature in the Star Wars universe.") @@ -75,24 +74,24 @@ public struct StarWarsAPI: API { Union(SearchResult.self, members: Planet.self, Human.self, Droid.self) Query { - Field("hero", at: StarWarsResolver.hero, as: Character.self) { + Field("hero", at: \.hero) { 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) { Argument("id", at: \.id) .description("Id of the human.") } - Field("droid", at: StarWarsResolver.droid) { + Field("droid", at: \.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") } 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 04ed574..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 = 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 [ From 377a539924d9f793cdeb9fb0f942d2fe345afd57 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:00:43 -0300 Subject: [PATCH 06/19] chore: Update CI to Swift 5.5 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 6faaba757cb3226b75792dd9f6a492f4ef64b9bc Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:06:28 -0300 Subject: [PATCH 07/19] chore: Fix GraphQl dependency in Package.swift --- Package.resolved | 9 +++++++++ Package.swift | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index b7ba3d2..41519aa 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "GraphQL", + "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", + "state": { + "branch": "feature/async-await", + "revision": "291bf038a2eb85c3a9484a7638c26ef5b2fff9eb", + "version": null + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections", diff --git a/Package.swift b/Package.swift index 14413a6..e9e3ccb 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( ], dependencies: [ // .package(url: "https://github.com/GraphQLSwift/GraphQL.git", .upToNextMajor(from: "2.0.0")), - .package(path: "../GraphQL") + .package(url: "https://github.com/GraphQLSwift/GraphQL.git", .branch("feature/async-await")), ], targets: [ .target(name: "Graphiti", dependencies: ["GraphQL"]), From d051b4ba52413c62851b0f52257958508d9ac9f0 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:49:26 -0300 Subject: [PATCH 08/19] refactor: Improve counter example --- README.md | 153 +++++++++++------- Sources/Graphiti/Context/NoContext.swift | 2 +- .../CounterTests/CounterTests.swift | 127 +++++++++------ Tests/GraphitiTests/ScalarTests.swift | 91 ++++++----- 4 files changed, 223 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index a89567c..ce6dc8c 100644 --- a/README.md +++ b/README.md @@ -26,106 +26,122 @@ through that README and the corresponding tests in parallel. Add Graphiti to your `Package.swift`. Graphiti provides two important capabilities: building a type schema, and serving queries against that type schema. -#### Defining entities +### Defining entities -First, we declare 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`. +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 Counter: Encodable { - var count: Int +struct Count: Encodable { + var value: Int } ``` ⭐️ Notice that this step does not require importing `Graphiti`. One of the main design decisions behind Graphiti is **not** to pollute your entities declarations. This way you can bring your entities to any other environment with ease. -#### Defining the context +#### Defining the business logic -Second step is to create your API's **context** actor. 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 derives your entities from a database or another service. +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 -actor CounterContext { - var counter: Counter +``` +actor CounterState { + var count: Count - init(counter: Counter) { - self.counter = counter + init(count: Count) { + self.count = count } - func increment() -> Counter { - counter.count += 1 - return counter + func increment() -> Count { + count.value += 1 + return count } - func decrement() -> Counter { - counter.count -= 1 - return counter + func decrement() -> Count { + count.value -= 1 + return count } - func increment(by count: Int) -> Counter { - counter.count += count - return counter + func increment(by amount: Int) -> Count { + count.value += amount + return count } - func decrement(by count: Int) -> Counter { - counter.count -= count - return counter + func decrement(by amount: Int) -> Count { + count.value -= amount + return count } } ``` -⭐️ Notice that this step does not require importing `Graphiti`. It is purely your API's business logic. +#### Defining the context + +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. 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. + +```swift +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 +} +``` #### Defining the GraphQL API resolver -Now that we have our entities and context we can declare the GraphQL API resolver. These resolver functions will be used to resolve the queries and mutations defined in the schema. +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 struct CounterResolver { - var counter: (CounterContext, Void) async throws -> Counter - var increment: (CounterContext, Void) async throws -> Counter - var decrement: (CounterContext, Void) async throws -> Counter + 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 count: Int + let amount: Int } - var incrementBy: (CounterContext, IncrementByArguments) async throws -> Counter + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Count struct DecrementByArguments: Decodable { - let count: Int + let amount: Int } - var decrementBy: (CounterContext, DecrementByArguments) async throws -> Counter + 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 thows -> Output where Arguments: Decodable, Output: Encodable +(Context, Arguments) async thows -> Output where Arguments: Decodable ``` In case your resolve function does not use any arguments you can use the following shape: ```swift -(Context, Void) async thows -> Output where Output: Encodable +(Context, Void) async thows -> 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 import Graphiti struct CounterAPI { let schema = Schema { - Type(Counter.self) { - Field("count", at: \.count) + Type(Count.self) { + Field("value", at: \.value) } Query { - Field("counter", at: \.counter) + Field("count", at: \.count) } Mutation { @@ -133,18 +149,18 @@ struct CounterAPI { Field("decrement", at: \.decrement) Field("incrementBy", at: \.incrementBy) { - Argument("count", at: \.count) + Argument("amount", at: \.amount) } Field("decrementBy", at: \.decrementBy) { - Argument("count", at: \.count) + Argument("amount", at: \.amount) } } } } ``` -⭐️ Now we finally import Graphiti. Notice that `Schema` allows dependency injection. You could pass mocks of `resolver` and `context` to `Schema.execute` when testing, for example. +⭐️ Now we finally need to import Graphiti. 😄 #### Querying @@ -152,7 +168,28 @@ To query the schema, we first need to create a live instance of the context: ```swift extension CounterContext { - static let live = CounterContext(counter: Counter(count: 0)) + static let live: CounterContext = { + let count = Count(value: 0) + let application = CounterState(count: count) + + return CounterContext( + count: { + await application.count + }, + increment: { + await application.increment() + }, + decrement: { + await application.decrement() + }, + incrementBy: { count in + await application.increment(by: count) + }, + decrementBy: { count in + await application.decrement(by: count) + } + ) + }() } ``` @@ -161,8 +198,8 @@ Now we create a live instance of the resolver: ```swift extension CounterResolver { static let live = CounterResolver( - counter: { context, _ in - await context.counter + count: { context, _ in + await context.count() }, increment: { context, _ in await context.increment() @@ -171,10 +208,10 @@ extension CounterResolver { await context.decrement() }, incrementBy: { context, arguments in - await context.increment(by: arguments.count) + await context.incrementBy(arguments.amount) }, decrementBy: { context, arguments in - await context.decrement(by: arguments.count) + await context.decrementBy(arguments.amount) } ) } @@ -195,8 +232,8 @@ let api = CounterAPI() let countQuery = """ query { - counter { - count + count { + value } } """ @@ -216,8 +253,8 @@ The output will be: ```json { "data" : { - "counter" : { - "count" : 0 + "count" : { + "value" : 0 } } } @@ -229,7 +266,7 @@ For the increment mutation: let incrementMutation = """ mutation { increment { - count + value } } """ @@ -250,7 +287,7 @@ The output will be: { "data" : { "increment" : { - "count" : 1 + "value" : 1 } } } @@ -262,7 +299,7 @@ For the decrement mutation: let decrementMutation = """ mutation { decrement { - count + value } } """ @@ -283,7 +320,7 @@ The output will be: { "data" : { "decrement" : { - "count" : 0 + "value" : 0 } } } @@ -295,7 +332,7 @@ For the incrementBy mutation: let incrementByMutation = """ mutation { incrementBy(count: 5) { - count + value } } """ @@ -316,7 +353,7 @@ The output will be: { "data" : { "incrementBy" : { - "count" : 5 + "value" : 5 } } } @@ -328,7 +365,7 @@ For the decrementBy mutation: let decrementByMutation = """ mutation { decrementBy(count: 5) { - count + value } } """ @@ -349,7 +386,7 @@ The output will be: { "data" : { "decrementBy" : { - "count" : 0 + "value" : 0 } } } diff --git a/Sources/Graphiti/Context/NoContext.swift b/Sources/Graphiti/Context/NoContext.swift index 5493cbe..960a940 100644 --- a/Sources/Graphiti/Context/NoContext.swift +++ b/Sources/Graphiti/Context/NoContext.swift @@ -1,2 +1,2 @@ -@available(*, deprecated, message: "Use the Void directly.") +@available(*, deprecated, message: "Use Void directly.") public typealias NoContext = Void diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 1a741b3..c3b09dd 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -4,67 +4,76 @@ import NIO @testable import Graphiti @available(macOS 12, *) -struct Counter: Encodable { - var count: Int +struct Count: Encodable { + var value: Int } @available(macOS 12, *) -actor CounterContext { - var counter: Counter +actor CounterState { + var count: Count - init(counter: Counter) { - self.counter = counter + init(count: Count) { + self.count = count } - func increment() -> Counter { - counter.count += 1 - return counter + func increment() -> Count { + count.value += 1 + return count } - func decrement() -> Counter { - counter.count -= 1 - return counter + func decrement() -> Count { + count.value -= 1 + return count } - func increment(by count: Int) -> Counter { - counter.count += count - return counter + func increment(by amount: Int) -> Count { + count.value += amount + return count } - func decrement(by count: Int) -> Counter { - counter.count -= count - return counter + 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 counter: (CounterContext, Void) async throws -> Counter - var increment: (CounterContext, Void) async throws -> Counter - var decrement: (CounterContext, Void) async throws -> Counter + 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 count: Int + let amount: Int } - var incrementBy: (CounterContext, IncrementByArguments) async throws -> Counter + var incrementBy: (CounterContext, IncrementByArguments) async throws -> Count struct DecrementByArguments: Decodable { - let count: Int + let amount: Int } - var decrementBy: (CounterContext, DecrementByArguments) async throws -> Counter + var decrementBy: (CounterContext, DecrementByArguments) async throws -> Count } @available(macOS 12, *) struct CounterAPI { let schema = Schema { - Type(Counter.self) { - Field("count", at: \.count) + Type(Count.self) { + Field("value", at: \.value) } Query { - Field("counter", at: \.counter) + Field("count", at: \.count) } Mutation { @@ -72,11 +81,11 @@ struct CounterAPI { Field("decrement", at: \.decrement) Field("incrementBy", at: \.incrementBy) { - Argument("count", at: \.count) + Argument("amount", at: \.amount) } Field("decrementBy", at: \.decrementBy) { - Argument("count", at: \.count) + Argument("amount", at: \.amount) } } } @@ -84,14 +93,35 @@ struct CounterAPI { @available(macOS 12, *) extension CounterContext { - static let live = CounterContext(counter: Counter(count: 0)) + static let live: CounterContext = { + let count = Count(value: 0) + let application = CounterState(count: count) + + return CounterContext( + count: { + await application.count + }, + increment: { + await application.increment() + }, + decrement: { + await application.decrement() + }, + incrementBy: { count in + await application.increment(by: count) + }, + decrementBy: { count in + await application.decrement(by: count) + } + ) + }() } @available(macOS 12, *) extension CounterResolver { static let live = CounterResolver( - counter: { context, _ in - await context.counter + count: { context, _ in + await context.count() }, increment: { context, _ in await context.increment() @@ -100,14 +130,15 @@ extension CounterResolver { await context.decrement() }, incrementBy: { context, arguments in - await context.increment(by: arguments.count) + await context.incrementBy(arguments.amount) }, decrementBy: { context, arguments in - await context.decrement(by: arguments.count) + await context.decrementBy(arguments.amount) } ) } +#warning("TODO: Move this to GraphQL") extension GraphQLResult: CustomDebugStringConvertible { public var debugDescription: String { let encoder = JSONEncoder() @@ -130,13 +161,13 @@ class CounterTests: XCTestCase { var query = """ query { - counter { - count + count { + value } } """ - var expected = GraphQLResult(data: ["counter": ["count": 0]]) + var expected = GraphQLResult(data: ["count": ["value": 0]]) var result = try api.schema.execute( request: query, @@ -151,11 +182,11 @@ class CounterTests: XCTestCase { query = """ mutation { increment { - count + value } } """ - expected = GraphQLResult(data: ["increment": ["count": 1]]) + expected = GraphQLResult(data: ["increment": ["value": 1]]) result = try api.schema.execute( request: query, @@ -170,11 +201,11 @@ class CounterTests: XCTestCase { query = """ mutation { decrement { - count + value } } """ - expected = GraphQLResult(data: ["decrement": ["count": 0]]) + expected = GraphQLResult(data: ["decrement": ["value": 0]]) result = try api.schema.execute( request: query, @@ -188,12 +219,12 @@ class CounterTests: XCTestCase { query = """ mutation { - incrementBy(count: 5) { - count + incrementBy(amount: 5) { + value } } """ - expected = GraphQLResult(data: ["incrementBy": ["count": 5]]) + expected = GraphQLResult(data: ["incrementBy": ["value": 5]]) result = try api.schema.execute( request: query, @@ -207,12 +238,12 @@ class CounterTests: XCTestCase { query = """ mutation { - decrementBy(count: 5) { - count + decrementBy(amount: 5) { + value } } """ - expected = GraphQLResult(data: ["decrementBy": ["count": 0]]) + expected = GraphQLResult(data: ["decrementBy": ["value": 0]]) result = try api.schema.execute( request: query, diff --git a/Tests/GraphitiTests/ScalarTests.swift b/Tests/GraphitiTests/ScalarTests.swift index 29c8c89..9b9cafc 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 = 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 = 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 = 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: [ @@ -689,7 +694,7 @@ class ScalarTests : XCTestCase { } } -fileprivate class TestAPI : API { +fileprivate class TestAPI: API { public let resolver: Resolver public let schema: Schema From b1f6ec31240d471ec23ff01e1599e41cedd62418 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:52:25 -0300 Subject: [PATCH 09/19] fix: Fix README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce6dc8c..f8cf2c3 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ struct Count: Encodable { } ``` -⭐️ Notice that this step does not require importing `Graphiti`. One of the main design decisions behind Graphiti is **not** to pollute your entities declarations. This way you can bring your entities to any other environment with ease. +⭐️ 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 the business logic 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 actor CounterState { var count: Count From a561f1781e246ded08caddb5ab8954eb3b363659 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:54:12 -0300 Subject: [PATCH 10/19] fix: Fix README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8cf2c3..4fed925 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ actor CounterState { #### Defining the context -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. 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. +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 CounterContext { @@ -86,6 +86,8 @@ struct CounterContext { } ``` +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 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. From 1e527a571c802c6545af333e38f03c3f492a5c31 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:56:52 -0300 Subject: [PATCH 11/19] fix: Fix README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4fed925..2ec6df0 100644 --- a/README.md +++ b/README.md @@ -115,14 +115,14 @@ struct CounterResolver { ⭐️ Notice that this step does not require importing `Graphiti`. However, all resolver functions must take the following shape: ```swift -(Context, Arguments) async thows -> Output where Arguments: Decodable +(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 thows -> Output +(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. From cf57da4ce30870753da96b6566c94b905001e29a Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 12:08:12 -0300 Subject: [PATCH 12/19] feature: Add SDL to README.md --- README.md | 24 ++++++++++++++++++++++++ Sources/Graphiti/Schema/Schema.swift | 2 ++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 2ec6df0..e333ac2 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,30 @@ struct CounterAPI { ⭐️ Now we finally need to import Graphiti. 😄 +The schema above is equivalent to the SDL: + +```graphql +type Count { + value: Int! +} + +type Query { + count: Count! +} + +type Mutation { + increment: Count! + decrement: Count! + incrementBy(amount: Int!): Count! + decrementBy(amount: Int!): Count! +} + +schema { + query: Query + mutation: Mutation +} +``` + #### Querying To query the schema, we first need to create a live instance of the context: diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index 101a422..faee9ca 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -25,6 +25,8 @@ public final class Schema { types: typeProvider.types, directives: typeProvider.directives ) + + print(self.schema) } } From 90cdabeff6f299197a5bd96dc08e07a4718e4344 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Sat, 30 Oct 2021 12:11:39 -0300 Subject: [PATCH 13/19] refactor: Fix counter state naming --- README.md | 14 +++++++------- .../GraphitiTests/CounterTests/CounterTests.swift | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e333ac2..6b40a7e 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ struct CounterAPI { ⭐️ Now we finally need to import Graphiti. 😄 -The schema above is equivalent to the SDL: +The schema above is equivalent to the following GraphQL SDL: ```graphql type Count { @@ -196,23 +196,23 @@ To query the schema, we first need to create a live instance of the context: extension CounterContext { static let live: CounterContext = { let count = Count(value: 0) - let application = CounterState(count: count) + let state = CounterState(count: count) return CounterContext( count: { - await application.count + await state.count }, increment: { - await application.increment() + await state.increment() }, decrement: { - await application.decrement() + await state.decrement() }, incrementBy: { count in - await application.increment(by: count) + await state.increment(by: count) }, decrementBy: { count in - await application.decrement(by: count) + await state.decrement(by: count) } ) }() diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index c3b09dd..09ceda8 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -95,23 +95,23 @@ struct CounterAPI { extension CounterContext { static let live: CounterContext = { let count = Count(value: 0) - let application = CounterState(count: count) + let state = CounterState(count: count) return CounterContext( count: { - await application.count + await state.count }, increment: { - await application.increment() + await state.increment() }, decrement: { - await application.decrement() + await state.decrement() }, incrementBy: { count in - await application.increment(by: count) + await state.increment(by: count) }, decrementBy: { count in - await application.decrement(by: count) + await state.decrement(by: count) } ) }() From 3ae8d0a4cfe75157463415ceac74eb9549314e60 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Mon, 1 Nov 2021 10:19:38 -0300 Subject: [PATCH 14/19] feature: String literal descriptions --- .gitignore | 8 +- Package.resolved | 2 +- Package.swift | 2 +- README.md | 14 ++-- Sources/Graphiti/Argument/Argument.swift | 13 ++++ .../Graphiti/Argument/ArgumentComponent.swift | 16 +++- Sources/Graphiti/Component/Component.swift | 16 +++- .../Graphiti/Connection/ConnectionType.swift | 12 +++ Sources/Graphiti/Enum/Enum.swift | 12 +++ Sources/Graphiti/Field/Field/Field.swift | 23 +++++- .../Graphiti/Field/Field/FieldComponent.swift | 24 +++++- Sources/Graphiti/Input/Input.swift | 12 +++ Sources/Graphiti/Interface/Interface.swift | 12 +++ Sources/Graphiti/Mutation/Mutation.swift | 12 +++ Sources/Graphiti/Query/Query.swift | 12 +++ Sources/Graphiti/Scalar/Scalar.swift | 25 +++--- Sources/Graphiti/Schema/Schema.swift | 8 +- .../Subscription/SubscribeField.swift | 13 ++++ .../Graphiti/Subscription/Subscription.swift | 12 +++ Sources/Graphiti/Type/Type.swift | 12 +++ Sources/Graphiti/Types/Types.swift | 12 +++ Sources/Graphiti/Union/Union.swift | 13 +++- Sources/Graphiti/Value/Value.swift | 24 +++++- .../CounterTests/CounterTests.swift | 1 + Tests/GraphitiTests/ScalarTests.swift | 1 + .../StarWarsAPI/StarWarsAPI.swift | 76 ++++++++++--------- 26 files changed, 319 insertions(+), 68 deletions(-) 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 41519aa..d671a13 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,7 +6,7 @@ "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", "state": { "branch": "feature/async-await", - "revision": "291bf038a2eb85c3a9484a7638c26ef5b2fff9eb", + "revision": "9037768ece5ba5a3b1105d8ff9736575404dc216", "version": null } }, diff --git a/Package.swift b/Package.swift index e9e3ccb..16dbe46 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.3 import PackageDescription let package = Package( diff --git a/README.md b/README.md index 6b40a7e..9972aa9 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,14 @@ struct CounterAPI { } ``` -⭐️ Now we finally need to import Graphiti. 😄 +⭐️ Now we finally need to import Graphiti 😄. To check the equivalent GraphQL SDL use: -The schema above is equivalent to the following GraphQL SDL: +```swift +let api = CounterAPI() +debugPrint(api.schema) +``` + +The output will be: ```graphql type Count { @@ -181,11 +186,6 @@ type Mutation { incrementBy(amount: Int!): Count! decrementBy(amount: Int!): Count! } - -schema { - query: Query - mutation: Mutation -} ``` #### Querying diff --git a/Sources/Graphiti/Argument/Argument.swift b/Sources/Graphiti/Argument/Argument.swift index f8d5c89..b8a9f32 100644 --- a/Sources/Graphiti/Argument/Argument.swift +++ b/Sources/Graphiti/Argument/Argument.swift @@ -16,6 +16,19 @@ public class Argument : ArgumentCompone init(name: String) { self.name = name + super.init() + } + + 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") } } diff --git a/Sources/Graphiti/Argument/ArgumentComponent.swift b/Sources/Graphiti/Argument/ArgumentComponent.swift index 87ed3ee..bb64f1c 100644 --- a/Sources/Graphiti/Argument/ArgumentComponent.swift +++ b/Sources/Graphiti/Argument/ArgumentComponent.swift @@ -1,11 +1,25 @@ 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(unicodeScalarLiteral string: String) { + self.description = string + } + + public required init(extendedGraphemeClusterLiteral string: String) { + self.description = string + } + + public required init(stringLiteral string: StringLiteralType) { + self.description = string + } } public extension ArgumentComponent { diff --git a/Sources/Graphiti/Component/Component.swift b/Sources/Graphiti/Component/Component.swift index 298d515..7c32059 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,6 +8,20 @@ open class Component { self.name = name } + public required init(unicodeScalarLiteral string: String) { + self.name = "" + self.description = string + } + + public required init(extendedGraphemeClusterLiteral string: String) { + self.name = "" + self.description = string + } + + public required init(stringLiteral string: StringLiteralType) { + self.name = "" + self.description = string + } func update(typeProvider: SchemaTypeProvider, coders: Coders) throws {} } 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/Enum/Enum.swift b/Sources/Graphiti/Enum/Enum.swift index a0f1e5b..b40b4a2 100644 --- a/Sources/Graphiti/Enum/Enum.swift +++ b/Sources/Graphiti/Enum/Enum.swift @@ -27,6 +27,18 @@ public final class Enum: FieldComponent: FieldComponent( @@ -60,6 +61,7 @@ public class Field: FieldComponent: FieldComponent ) { let syncResolve: SyncResolve = { type in diff --git a/Sources/Graphiti/Field/Field/FieldComponent.swift b/Sources/Graphiti/Field/Field/FieldComponent.swift index 6f6f950..80c1daf 100644 --- a/Sources/Graphiti/Field/Field/FieldComponent.swift +++ b/Sources/Graphiti/Field/Field/FieldComponent.swift @@ -1,12 +1,26 @@ 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) { fatalError() } + + init() {} + + public required init(unicodeScalarLiteral string: String) { + self.description = string + } + + public required init(extendedGraphemeClusterLiteral string: String) { + self.description = string + } + + public required init(stringLiteral string: StringLiteralType) { + self.description = string + } } public extension FieldComponent { @@ -15,8 +29,16 @@ public extension FieldComponent { return self } + @available(*, deprecated, message: "Use deprecated(reason:).") + @discardableResult func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } + + @discardableResult + 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/Interface/Interface.swift b/Sources/Graphiti/Interface/Interface.swift index e4f2797..668f9f9 100644 --- a/Sources/Graphiti/Interface/Interface.swift +++ b/Sources/Graphiti/Interface/Interface.swift @@ -33,6 +33,18 @@ public final class Interface : Component : 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..2890ecd 100644 --- a/Sources/Graphiti/Query/Query.swift +++ b/Sources/Graphiti/Query/Query.swift @@ -34,6 +34,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 { types: typeProvider.types, directives: typeProvider.directives ) - - print(self.schema) + } +} + +extension Schema: CustomDebugStringConvertible { + public var debugDescription: String { + printSchema(schema: schema) } } diff --git a/Sources/Graphiti/Subscription/SubscribeField.swift b/Sources/Graphiti/Subscription/SubscribeField.swift index 48a7dde..e6113e4 100644 --- a/Sources/Graphiti/Subscription/SubscribeField.swift +++ b/Sources/Graphiti/Subscription/SubscribeField.swift @@ -65,6 +65,7 @@ public class SubscriptionField( @@ -171,6 +172,18 @@ public class SubscriptionField : Component : Component : 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 where EnumType.RawValue == String { +public final class Value: ExpressibleByStringLiteral where EnumType.RawValue == String { let value: EnumType var description: String? var deprecationReason: String? @@ -8,6 +8,21 @@ public final class Value where EnumType ) { self.value = value } + + public required init(unicodeScalarLiteral string: String) { + self.value = EnumType(rawValue: "")! + self.description = string + } + + public required init(extendedGraphemeClusterLiteral string: String) { + self.value = EnumType(rawValue: "")! + self.description = string + } + + public required init(stringLiteral string: StringLiteralType) { + self.value = EnumType(rawValue: "")! + self.description = string + } } public extension Value { @@ -21,9 +36,16 @@ public extension Value { return self } + @available(*, deprecated, message: "Use deprecated(reason:).") @discardableResult func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } + + @discardableResult + func deprecated(reason deprecationReason: String) -> Self { + self.deprecationReason = deprecationReason + return self + } } diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 09ceda8..f4006dc 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -68,6 +68,7 @@ struct CounterResolver { @available(macOS 12, *) struct CounterAPI { let schema = Schema { + "Description" Type(Count.self) { Field("value", at: \.value) } diff --git a/Tests/GraphitiTests/ScalarTests.swift b/Tests/GraphitiTests/ScalarTests.swift index 9b9cafc..f979c80 100644 --- a/Tests/GraphitiTests/ScalarTests.swift +++ b/Tests/GraphitiTests/ScalarTests.swift @@ -200,6 +200,7 @@ class ScalarTests: XCTestCase { Field("date", at: TestResolver.date) } } + let api = TestAPI ( resolver: TestResolver(), schema: testSchema diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index 306a984..389881c 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -3,36 +3,37 @@ import Graphiti @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) + "The friends of the character, or an empty list if they have none." Field("friends", at: \.friends, as: [TypeReference].self) - .description("The friends of the character, or an empty list if they have none.") - 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) @@ -41,54 +42,55 @@ public struct StarWarsAPI { Field("orbitalPeriod", at: \.orbitalPeriod) Field("residents", at: \.residents, as: [TypeReference].self) } - .description("A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY.") + "A humanoid creature in the Star Wars universe." Type(Human.self, implements: Character.self) { - Field("id", at: \.id) - Field("name", at: \.name) - Field("appearsIn", at: \.appearsIn) - Field("homePlanet", at: \.homePlanet) + 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.init("friends", at: \.getFriends) - .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) + "Where are they from and how they came to be who they are." Field("secretBackstory", at: \.getSecretBackstory) - .description("Where are they from and how they came to be who they are.") } - .description("A humanoid creature in the Star Wars universe.") + "A mechanical creature in the Star Wars universe." Type(Droid.self, implements: Character.self) { - Field("id", at: \.id) - Field("name", at: \.name) - Field("appearsIn", at: \.appearsIn) - Field("primaryFunction", at: \.primaryFunction) + 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) + "The friends of the droid, or an empty list if they have none." Field("friends", at: \.getFriends) - .description("The friends of the droid, or an empty list if they have none.") + "Where are they from and how they came to be who they are." Field("secretBackstory", at: \.getSecretBackstory) - .description("Where are they from and how they came to be who they are.") } - .description("A mechanical creature in the Star Wars universe.") Union(SearchResult.self, members: Planet.self, Human.self, Droid.self) Query { + "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: \.human) { + "Id of the human." Argument("id", at: \.id) - .description("Id of the human.") } Field("droid", at: \.droid) { + "Id of the droid." Argument("id", at: \.id) - .description("Id of the droid.") } Field("search", at: \.search) { @@ -97,6 +99,8 @@ public struct StarWarsAPI { } } - 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) } } + From 69a2cbd088480cb665f8e99351d719ad6c8f44cf Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:26:12 -0300 Subject: [PATCH 15/19] feature: Start directive feature --- .../Graphiti/Argument/ArgumentComponent.swift | 1 + Sources/Graphiti/Component/Component.swift | 1 + Sources/Graphiti/Directive/Directive.swift | 62 +++++++++++++++++++ Sources/Graphiti/Field/Field/Field.swift | 15 +++++ .../Graphiti/Field/Field/FieldComponent.swift | 3 +- .../InputField/InputFieldComponent.swift | 1 + Sources/Graphiti/Query/Query.swift | 6 ++ Sources/Graphiti/Schema/Schema.swift | 4 +- .../Graphiti/Schema/SchemaTypeProvider.swift | 2 +- Sources/Graphiti/Value/Value.swift | 4 +- .../StarWarsAPI/StarWarsAPI.swift | 14 ++--- 11 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 Sources/Graphiti/Directive/Directive.swift diff --git a/Sources/Graphiti/Argument/ArgumentComponent.swift b/Sources/Graphiti/Argument/ArgumentComponent.swift index bb64f1c..cc20f05 100644 --- a/Sources/Graphiti/Argument/ArgumentComponent.swift +++ b/Sources/Graphiti/Argument/ArgumentComponent.swift @@ -23,6 +23,7 @@ public class ArgumentComponent: ExpressibleByStringLi } 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/Component/Component.swift b/Sources/Graphiti/Component/Component.swift index 7c32059..2f87d62 100644 --- a/Sources/Graphiti/Component/Component.swift +++ b/Sources/Graphiti/Component/Component.swift @@ -27,6 +27,7 @@ open class Component: ExpressibleByStringLiteral { } 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/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift new file mode 100644 index 0000000..8b8d920 --- /dev/null +++ b/Sources/Graphiti/Directive/Directive.swift @@ -0,0 +1,62 @@ +import GraphQL +#warning("TODO: Create Directive component") + +struct ArgumentConfiguration { + let name: String + var defaultValue: AnyEncodable? +} + +struct FieldConfiguration { + var name: String + var description: String? + var deprecationReason: String? + var arguments: [ArgumentConfiguration] + var resolve: AsyncResolve +} + +protocol FieldDefinitionDirective { + associatedtype Context + func fieldDefinition(fieldDefinition: inout FieldConfiguration) +} + +struct Deprecated: FieldDefinitionDirective { + let reason: String + + func fieldDefinition(fieldDefinition: inout FieldConfiguration) { + fieldDefinition.deprecationReason = reason + } +} + +func directive() throws { + // + // "Marks a field or enum value as deprecated" + // Directive(Deprecated.self) { + // "The reason for the deprecation" + // Argument("reason", at: \.reason) + // .defaultValue("No longer supported") + // } on: { + // Location(\.fieldDefinition) + // Location(\.enumValue) + // } + // .repeatable() + // + // + // Type(User.self) { + // Field("age", of: Int.self) + // .directive(Deprecated(reason: "Use dateOfBirth instead")) // We need to keep track of the directives applied to check for "repeatable" + // } + // + + let directive = try GraphQLDirective( + name: "deprecated", + description: "Marks a field as deprecated", + locations: [.fieldDefinition, .enumValue], + args: [ + "reason": GraphQLArgument( + type: GraphQLString, + description: "The reason for the deprecation", + defaultValue: .string("No longer supported") + ) + ] + ) +} diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 17d0450..34fd7ec 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -361,6 +361,7 @@ public extension Field where Arguments == NoArguments, FieldType: Encodable { } public extension Field where Arguments == NoArguments { + @available(*, deprecated, message: "Use the Field.init(_:of:at:) instead.") convenience init( _ name: String, at keyPath: KeyPath, @@ -374,4 +375,18 @@ public extension Field where Arguments == NoArguments { self.init(name: name, arguments: [], syncResolve: syncResolve) } + + convenience init( + _ name: String, + of: FieldType.Type, + at keyPath: KeyPath + ) where ResolveType: Encodable { + let syncResolve: SyncResolve = { type in + { _, _ in + type[keyPath: keyPath] + } + } + + self.init(name: name, arguments: [], syncResolve: syncResolve) + } } diff --git a/Sources/Graphiti/Field/Field/FieldComponent.swift b/Sources/Graphiti/Field/Field/FieldComponent.swift index 80c1daf..e661ea3 100644 --- a/Sources/Graphiti/Field/Field/FieldComponent.swift +++ b/Sources/Graphiti/Field/Field/FieldComponent.swift @@ -24,19 +24,18 @@ public class FieldComponent: ExpressibleByStringLiteral { } 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:).") - @discardableResult func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } - @discardableResult func deprecated(reason deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self 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/Query/Query.swift b/Sources/Graphiti/Query/Query.swift index 2890ecd..9b8ec75 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, diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index a697c19..00f0c67 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -15,7 +15,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( diff --git a/Sources/Graphiti/Schema/SchemaTypeProvider.swift b/Sources/Graphiti/Schema/SchemaTypeProvider.swift index da9dd12..5b32d26 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, diff --git a/Sources/Graphiti/Value/Value.swift b/Sources/Graphiti/Value/Value.swift index 3bd5a12..1e9a701 100644 --- a/Sources/Graphiti/Value/Value.swift +++ b/Sources/Graphiti/Value/Value.swift @@ -30,20 +30,18 @@ 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 } @available(*, deprecated, message: "Use deprecated(reason:).") - @discardableResult func deprecationReason(_ deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self } - @discardableResult func deprecated(reason deprecationReason: String) -> Self { self.deprecationReason = deprecationReason return self diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift index 389881c..274654d 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsAPI.swift @@ -24,7 +24,7 @@ public struct StarWarsAPI { Field("name", of: String.self, at: \.name) "The friends of the character, or an empty list if they have none." - Field("friends", at: \.friends, as: [TypeReference].self) + Field("friends", of: [TypeReference].self, at: \.friends) "Which movies they appear in." Field("appearsIn", of: [Episode].self, at: \.appearsIn) @@ -35,12 +35,12 @@ public struct StarWarsAPI { "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) } "A humanoid creature in the Star Wars universe." From 1581a7f1b8d8cf235d4be91fe1408450334eb323 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Mon, 1 Nov 2021 18:49:03 -0300 Subject: [PATCH 16/19] feature: Server directive support --- Package.resolved | 2 +- Sources/Graphiti/Argument/Argument.swift | 9 ++ Sources/Graphiti/Directive/Directive.swift | 143 +++++++++++------- .../Directive/FieldDefinitionDirective.swift | 18 +++ .../DirectiveLocation/DirectiveLocation.swift | 15 ++ .../DirectiveLocationBuilder.swift | 11 ++ Sources/Graphiti/Field/Field/Field.swift | 61 +++++++- Sources/Graphiti/Value/Value.swift | 1 + .../CounterTests/CounterTests.swift | 93 ++++++++++++ 9 files changed, 296 insertions(+), 57 deletions(-) create mode 100644 Sources/Graphiti/Directive/FieldDefinitionDirective.swift create mode 100644 Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift create mode 100644 Sources/Graphiti/DirectiveLocation/DirectiveLocationBuilder.swift diff --git a/Package.resolved b/Package.resolved index d671a13..e20b1a1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,7 +6,7 @@ "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", "state": { "branch": "feature/async-await", - "revision": "9037768ece5ba5a3b1105d8ff9736575404dc216", + "revision": "db8a7fc3a4c543cc2eb3bb4139a16b7e0b43fd13", "version": null } }, diff --git a/Sources/Graphiti/Argument/Argument.swift b/Sources/Graphiti/Argument/Argument.swift index b8a9f32..a186410 100644 --- a/Sources/Graphiti/Argument/Argument.swift +++ b/Sources/Graphiti/Argument/Argument.swift @@ -33,12 +33,21 @@ public class Argument : ArgumentCompone } 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) + } } public extension Argument where ArgumentType : Encodable { diff --git a/Sources/Graphiti/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift index 8b8d920..bd96e74 100644 --- a/Sources/Graphiti/Directive/Directive.swift +++ b/Sources/Graphiti/Directive/Directive.swift @@ -1,62 +1,101 @@ import GraphQL -#warning("TODO: Create Directive component") -struct ArgumentConfiguration { - let name: String - var defaultValue: AnyEncodable? +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") + typeProvider.directives.append(directive) + } + + 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") + } } -struct FieldConfiguration { - var name: String - var description: String? - var deprecationReason: String? - var arguments: [ArgumentConfiguration] - var resolve: AsyncResolve +extension StringProtocol { + func firstCharacterLowercased() -> String { + prefix(1).lowercased() + dropFirst() + } } -protocol FieldDefinitionDirective { - associatedtype Context - func fieldDefinition(fieldDefinition: inout FieldConfiguration) +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() + ) + } } -struct Deprecated: FieldDefinitionDirective { - let reason: String - func fieldDefinition(fieldDefinition: inout FieldConfiguration) { - fieldDefinition.deprecationReason = reason - } -} -func directive() throws { - // - // "Marks a field or enum value as deprecated" - // Directive(Deprecated.self) { - // "The reason for the deprecation" - // Argument("reason", at: \.reason) - // .defaultValue("No longer supported") - // } on: { - // Location(\.fieldDefinition) - // Location(\.enumValue) - // } - // .repeatable() - // - // - // Type(User.self) { - // Field("age", of: Int.self) - // .directive(Deprecated(reason: "Use dateOfBirth instead")) // We need to keep track of the directives applied to check for "repeatable" - // } - // - - let directive = try GraphQLDirective( - name: "deprecated", - description: "Marks a field as deprecated", - locations: [.fieldDefinition, .enumValue], - args: [ - "reason": GraphQLArgument( - type: GraphQLString, - description: "The reason for the deprecation", - defaultValue: .string("No longer supported") - ) - ] - ) -} + diff --git a/Sources/Graphiti/Directive/FieldDefinitionDirective.swift b/Sources/Graphiti/Directive/FieldDefinitionDirective.swift new file mode 100644 index 0000000..c15ddf1 --- /dev/null +++ b/Sources/Graphiti/Directive/FieldDefinitionDirective.swift @@ -0,0 +1,18 @@ +import GraphQL + +public struct ArgumentConfiguration { + public let name: String + public var defaultValue: Map? +} + +public struct FieldConfiguration { + public let name: String + public var description: String? + public var deprecationReason: String? + public var arguments: [ArgumentConfiguration] + public var resolve: AsyncResolve +} + +public protocol FieldDefinitionDirective { + var fieldDefinition: (inout FieldConfiguration) -> Void { get } +} diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift new file mode 100644 index 0000000..78da89f --- /dev/null +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift @@ -0,0 +1,15 @@ +import GraphQL + +public final class DirectiveLocation { + let location: GraphQL.DirectiveLocation + + init(location: GraphQL.DirectiveLocation) { + self.location = location + } +} + +public extension DirectiveLocation { + convenience init(_ location: KeyPath Void>) { + self.init(location: .fieldDefinition) + } +} 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/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 34fd7ec..7a05917 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -4,14 +4,18 @@ import NIO 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) { + let arguments = try arguments(typeProvider: typeProvider, coders: coders) + applyDirectives(typeProdiver: typeProvider, arguments: arguments) + let field = GraphQLField( type: try typeProvider.getOutputType(from: FieldType.self, field: name), description: description, deprecationReason: deprecationReason, - args: try arguments(typeProvider: typeProvider, coders: coders), + args: arguments, resolve: { source, arguments, context, eventLoopGroup, _ in guard let source = source as? ObjectType else { throw GraphQLError(message: "Expected source type \(ObjectType.self) but got \(type(of: source))") @@ -40,6 +44,46 @@ public class Field: FieldComponent], @@ -347,7 +391,7 @@ public extension Field where Arguments == NoArguments { public extension Field where Arguments == NoArguments, FieldType: Encodable { convenience init( _ name: String, - of: FieldType.Type = FieldType.self, + of type: FieldType.Type = FieldType.self, at keyPath: KeyPath ) { let syncResolve: SyncResolve = { type in @@ -378,7 +422,7 @@ public extension Field where Arguments == NoArguments { convenience init( _ name: String, - of: FieldType.Type, + of type: FieldType.Type, at keyPath: KeyPath ) where ResolveType: Encodable { let syncResolve: SyncResolve = { type in @@ -390,3 +434,12 @@ public extension Field where Arguments == NoArguments { 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/Value/Value.swift b/Sources/Graphiti/Value/Value.swift index 1e9a701..05edcdb 100644 --- a/Sources/Graphiti/Value/Value.swift +++ b/Sources/Graphiti/Value/Value.swift @@ -1,3 +1,4 @@ +#warning("TODO: Rename to EnumValue") public final class Value: ExpressibleByStringLiteral where EnumType.RawValue == String { let value: EnumType var description: String? diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index f4006dc..6d80542 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -149,6 +149,8 @@ extension GraphQLResult: CustomDebugStringConvertible { } } + + @available(macOS 12, *) class CounterTests: XCTestCase { private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) @@ -157,6 +159,97 @@ class CounterTests: XCTestCase { try? self.group.syncShutdownGracefully() } + func testDirective() throws { + struct User: Encodable { + let name: String + } + + struct Resolver { + let user: User + } + + #warning("TODO: Move Decodable requirement from ArgumentComponent to Argument") + struct Lowercased: Decodable, FieldDefinitionDirective { + var fieldDefinition: (inout FieldConfiguration) -> Void { + { fieldDefinition in + let resolve = fieldDefinition.resolve + + fieldDefinition.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() + } + } + } + } + } + } + + struct Uppercased: Decodable, FieldDefinitionDirective { + var fieldDefinition: (inout FieldConfiguration) -> Void { + { fieldDefinition in + let resolve = fieldDefinition.resolve + + fieldDefinition.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() + } + } + } + } + } + } + + let schema = Schema { + // Directive(DeprecatedBy.self) { + // Argument("reason", of: String.self, at: \.reason) + // .defaultValue("No longer supported") + // } on: { + // DirectiveLocation(\.fieldDefinition) + // } + + Directive(Lowercased.self) { + DirectiveLocation(\.fieldDefinition) + } + + Directive(Uppercased.self) { + DirectiveLocation(\.fieldDefinition) + } + + Type(User.self) { + Field("name", of: String.self, at: \.name) + .directive(Uppercased()) + .directive(Lowercased()) + } + + Query { + Field("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 } }", + resolver: Resolver(user: User(name: "Paulo")), + context: (), + on: group + ).wait() + + + debugPrint(result) + } + func testCounter() throws { let api = CounterAPI() From 1b1bd096ed9063cf1fbef54474291d811c04647e Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Tue, 2 Nov 2021 11:40:00 -0300 Subject: [PATCH 17/19] feature: Experiment with directive support --- Sources/Graphiti/Directive/Directive.swift | 1 + .../Directive/FieldDefinitionDirective.swift | 34 +++++- .../Graphiti/Directive/ObjectDirective.swift | 63 +++++++++++ .../DirectiveLocation/DirectiveLocation.swift | 32 +++++- Sources/Graphiti/Field/Field/Field.swift | 78 +++++++------- Sources/Graphiti/Schema/Schema.swift | 1 + Sources/Graphiti/Type/Type.swift | 33 +++++- .../CounterTests/CounterTests.swift | 102 +++++++++++------- 8 files changed, 258 insertions(+), 86 deletions(-) create mode 100644 Sources/Graphiti/Directive/ObjectDirective.swift diff --git a/Sources/Graphiti/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift index bd96e74..c363ef7 100644 --- a/Sources/Graphiti/Directive/Directive.swift +++ b/Sources/Graphiti/Directive/Directive.swift @@ -1,5 +1,6 @@ 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] diff --git a/Sources/Graphiti/Directive/FieldDefinitionDirective.swift b/Sources/Graphiti/Directive/FieldDefinitionDirective.swift index c15ddf1..9337f57 100644 --- a/Sources/Graphiti/Directive/FieldDefinitionDirective.swift +++ b/Sources/Graphiti/Directive/FieldDefinitionDirective.swift @@ -1,18 +1,48 @@ import GraphQL +#warning("TODO: Move to ArgumentDefinitionDirective") public struct ArgumentConfiguration { public let name: String public var defaultValue: Map? } +extension ArgumentConfiguration { + init(_ argumentDefinition: GraphQLArgumentDefinition) { + self.name = argumentDefinition.name + self.defaultValue = argumentDefinition.defaultValue + } +} + public struct FieldConfiguration { public let name: String public var description: String? public var deprecationReason: String? public var arguments: [ArgumentConfiguration] - public var resolve: AsyncResolve + #warning("TODO: Think about how to improve this ergonomics. Maybe hide the original in a wrapper to be able to unwrap it?") + public var resolve: GraphQLFieldResolve +} + +extension FieldConfiguration { + init(_ pair: (String, GraphQLFieldDefinition)) { + let (name, fieldDefinition) = pair + self.name = name + self.description = fieldDefinition.description + self.deprecationReason = fieldDefinition.deprecationReason + self.arguments = fieldDefinition.args.map(ArgumentConfiguration.init) + // We're guarateed to have resolve because the Graphiti.Field implementation always + // provides a resolve function + self.resolve = fieldDefinition.resolve! + } +} + +public struct ConfigureFieldDefinition { + let configure: (inout FieldConfiguration) -> Void + + init(configure: @escaping (inout FieldConfiguration) -> Void) { + self.configure = configure + } } public protocol FieldDefinitionDirective { - var fieldDefinition: (inout FieldConfiguration) -> Void { get } + var fieldDefinition: ConfigureFieldDefinition { get } } diff --git a/Sources/Graphiti/Directive/ObjectDirective.swift b/Sources/Graphiti/Directive/ObjectDirective.swift new file mode 100644 index 0000000..2d8ec0f --- /dev/null +++ b/Sources/Graphiti/Directive/ObjectDirective.swift @@ -0,0 +1,63 @@ +import GraphQL + +#warning("TODO: Move to InterfaceDirective") +public struct InterfaceConfiguration { + public let name: String + public var description: String? + public var fields: [FieldConfiguration] + public var interfaces: [InterfaceConfiguration] +} + +extension InterfaceConfiguration { + init(_ interface: GraphQLInterfaceType) { + self.name = interface.name + self.description = interface.description + self.fields = interface.fields.map(FieldConfiguration.init) + self.interfaces = interface.interfaces.map(InterfaceConfiguration.init) + } +} + +public struct ObjectConfiguration { + public let name: String + public var description: String? + public var fields: [FieldConfiguration] + public var interfaces: [InterfaceConfiguration] + fileprivate let isTypeOf: GraphQLIsTypeOf? +} + +extension ObjectConfiguration { + init(_ objectType: GraphQLObjectType) { + self.name = objectType.name + self.description = objectType.description + self.fields = objectType.fields.map(FieldConfiguration.init) + self.interfaces = objectType.interfaces.map(InterfaceConfiguration.init) + self.isTypeOf = objectType.isTypeOf + + } +} + +extension GraphQLObjectType { + convenience init(_ configuration: ObjectConfiguration) throws { + try self.init( + name: configuration.name, + description: configuration.description, + fields: [:], //configuration.fields.reduce(into: [:]) { result, configuration in +// result[configuration.name] = GraphQLFieldDefinition(configuration) +// }, + interfaces: [], // configuration.interfaces.map(GraphQLInterfaceType.init), + isTypeOf: configuration.isTypeOf + ) + } +} + +public struct ConfigureObject { + let configure: (inout ObjectConfiguration) -> Void + + init(configure: @escaping (inout ObjectConfiguration) -> Void) { + self.configure = configure + } +} + +public protocol ObjectDirective { + var object: ConfigureObject { get } +} diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift index 78da89f..5ef21ac 100644 --- a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift @@ -8,8 +8,38 @@ public final class DirectiveLocation { } } +// MARK: - Schema + +// MARK: - Scalar + +// MARK: - Object + +public struct ObjectDirectiveLocation { + public static let object = ObjectDirectiveLocation() +} + public extension DirectiveLocation { - convenience init(_ location: KeyPath Void>) { + convenience init(_ location: ObjectDirectiveLocation, at keyPath: KeyPath) { + self.init(location: .object) + } +} + +// MARK: - FieldDefinition + +public struct FieldDefinitionDirectiveLocation { + public static let fieldDefinition = FieldDefinitionDirectiveLocation() +} + +public extension DirectiveLocation { + convenience init(_ location: FieldDefinitionDirectiveLocation, at keyPath: KeyPath) { self.init(location: .fieldDefinition) } } + +// MARK: - ArgumentDefinition +// MARK: - Interface +// MARK: - Union +// MARK: - Enum +// MARK: - EnumValue +// MARK: - InputObject +// MARK: - InputFieldDefinition diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 7a05917..7c449a9 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -9,7 +9,7 @@ public class Field: FieldComponent (String, GraphQLField) { let arguments = try arguments(typeProvider: typeProvider, coders: coders) - applyDirectives(typeProdiver: typeProvider, arguments: arguments) + applyDirectives(arguments: arguments) let field = GraphQLField( type: try typeProvider.getOutputType(from: FieldType.self, field: name), @@ -44,44 +44,44 @@ public class Field: FieldComponent { public let schema: GraphQLSchema diff --git a/Sources/Graphiti/Type/Type.swift b/Sources/Graphiti/Type/Type.swift index fba1906..2e8ece7 100644 --- a/Sources/Graphiti/Type/Type.swift +++ b/Sources/Graphiti/Type/Type.swift @@ -1,25 +1,27 @@ import GraphQL -public final class Type : 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 { 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 ) - try typeProvider.map(ObjectType.self, to: objectType) + let mappedObjectType = applyDirectives(objectType: objectType) + try typeProvider.map(ObjectType.self, to: mappedObjectType) } func fields(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLFieldMap { @@ -33,6 +35,18 @@ public final class Type : Component GraphQLObjectType { +// var objectType = objectType +// +// for directive in directives { +// var objectConfiguration = ObjectConfiguration(objectType) +// directive.object.configure(&objectConfiguration) +// objectType = GraphQLObjectType(objectConfiguration) +// } +// + return objectType + } + private init( type: ObjectType.Type, name: String?, @@ -118,3 +132,12 @@ public extension Type { ) } } + +// MARK: Directive + +extension Type { + func directive(_ directive: Directive) -> Type where Directive: ObjectDirective { + directives.append(directive) + return self + } +} diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index 6d80542..df611c1 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -168,61 +168,84 @@ class CounterTests: XCTestCase { let user: User } + struct DelegateField: Decodable, ObjectDirective { + let name: String + + var object: ConfigureObject { + ConfigureObject { configuration in +// configuration.fields.append( +// FieldConfiguration( +// name: name, +// description: nil, +// deprecationReason: nil, +// arguments: [], +// resolve: { object in +// { _, _, group in +// group.next().makeCompletedFuture(.success("Yo")) +// } +// } +// ) +// ) + } + } + } + #warning("TODO: Move Decodable requirement from ArgumentComponent to Argument") struct Lowercased: Decodable, FieldDefinitionDirective { - var fieldDefinition: (inout FieldConfiguration) -> Void { - { fieldDefinition in - let resolve = fieldDefinition.resolve - - fieldDefinition.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() - } - } - } + var fieldDefinition: ConfigureFieldDefinition { + ConfigureFieldDefinition { fieldDefinition in +// let resolve = fieldDefinition.resolve +// +// fieldDefinition.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() +// } +// } +// } } } } struct Uppercased: Decodable, FieldDefinitionDirective { - var fieldDefinition: (inout FieldConfiguration) -> Void { - { fieldDefinition in - let resolve = fieldDefinition.resolve - - fieldDefinition.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() - } - } - } + var fieldDefinition: ConfigureFieldDefinition { + ConfigureFieldDefinition { fieldDefinition in +// let resolve = fieldDefinition.resolve +// +// fieldDefinition.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() +// } +// } +// } } } } let schema = Schema { - // Directive(DeprecatedBy.self) { - // Argument("reason", of: String.self, at: \.reason) - // .defaultValue("No longer supported") - // } on: { - // DirectiveLocation(\.fieldDefinition) - // } + Directive(DelegateField.self) { + Argument("name", of: String.self, at: \.name) + } on: { + DirectiveLocation(.object, at: \.object) +// DirectiveLocation(.interface, at: \.interface) + } +// .repeatable() Directive(Lowercased.self) { - DirectiveLocation(\.fieldDefinition) + DirectiveLocation(.fieldDefinition, at: \.fieldDefinition) } Directive(Uppercased.self) { - DirectiveLocation(\.fieldDefinition) + DirectiveLocation(.fieldDefinition, at: \.fieldDefinition) } Type(User.self) { @@ -230,9 +253,10 @@ class CounterTests: XCTestCase { .directive(Uppercased()) .directive(Lowercased()) } + .directive(DelegateField(name: "salute")) Query { - Field("user", of: User.self, at: \.user) + Field.init("user", of: User.self, at: \.user) } } From 2285f01b65a56e8cdb6f7dfdaf16d3f15cc774be Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Tue, 2 Nov 2021 21:29:23 -0300 Subject: [PATCH 18/19] feature: Improve directives --- Sources/Graphiti/Directive/Directive.swift | 1 + .../Directive/FieldDefinitionDirective.swift | 48 ------- .../Graphiti/Directive/ObjectDirective.swift | 63 --------- .../DirectiveLocation/DirectiveLocation.swift | 59 ++++++++- Sources/Graphiti/Field/Field/Field.swift | 59 +++------ .../Graphiti/Field/Field/FieldComponent.swift | 2 +- Sources/Graphiti/Interface/Interface.swift | 20 ++- Sources/Graphiti/Mutation/Mutation.swift | 2 +- Sources/Graphiti/Query/Query.swift | 2 +- .../Subscription/SubscribeField.swift | 2 +- .../Graphiti/Subscription/Subscription.swift | 2 +- Sources/Graphiti/Type/Type.swift | 21 ++- .../CounterTests/CounterTests.swift | 125 ++++++++++-------- 13 files changed, 173 insertions(+), 233 deletions(-) delete mode 100644 Sources/Graphiti/Directive/FieldDefinitionDirective.swift delete mode 100644 Sources/Graphiti/Directive/ObjectDirective.swift diff --git a/Sources/Graphiti/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift index c363ef7..6eff39c 100644 --- a/Sources/Graphiti/Directive/Directive.swift +++ b/Sources/Graphiti/Directive/Directive.swift @@ -19,6 +19,7 @@ public final class Directive: Component Void - - init(configure: @escaping (inout FieldConfiguration) -> Void) { - self.configure = configure - } -} - -public protocol FieldDefinitionDirective { - var fieldDefinition: ConfigureFieldDefinition { get } -} diff --git a/Sources/Graphiti/Directive/ObjectDirective.swift b/Sources/Graphiti/Directive/ObjectDirective.swift deleted file mode 100644 index 2d8ec0f..0000000 --- a/Sources/Graphiti/Directive/ObjectDirective.swift +++ /dev/null @@ -1,63 +0,0 @@ -import GraphQL - -#warning("TODO: Move to InterfaceDirective") -public struct InterfaceConfiguration { - public let name: String - public var description: String? - public var fields: [FieldConfiguration] - public var interfaces: [InterfaceConfiguration] -} - -extension InterfaceConfiguration { - init(_ interface: GraphQLInterfaceType) { - self.name = interface.name - self.description = interface.description - self.fields = interface.fields.map(FieldConfiguration.init) - self.interfaces = interface.interfaces.map(InterfaceConfiguration.init) - } -} - -public struct ObjectConfiguration { - public let name: String - public var description: String? - public var fields: [FieldConfiguration] - public var interfaces: [InterfaceConfiguration] - fileprivate let isTypeOf: GraphQLIsTypeOf? -} - -extension ObjectConfiguration { - init(_ objectType: GraphQLObjectType) { - self.name = objectType.name - self.description = objectType.description - self.fields = objectType.fields.map(FieldConfiguration.init) - self.interfaces = objectType.interfaces.map(InterfaceConfiguration.init) - self.isTypeOf = objectType.isTypeOf - - } -} - -extension GraphQLObjectType { - convenience init(_ configuration: ObjectConfiguration) throws { - try self.init( - name: configuration.name, - description: configuration.description, - fields: [:], //configuration.fields.reduce(into: [:]) { result, configuration in -// result[configuration.name] = GraphQLFieldDefinition(configuration) -// }, - interfaces: [], // configuration.interfaces.map(GraphQLInterfaceType.init), - isTypeOf: configuration.isTypeOf - ) - } -} - -public struct ConfigureObject { - let configure: (inout ObjectConfiguration) -> Void - - init(configure: @escaping (inout ObjectConfiguration) -> Void) { - self.configure = configure - } -} - -public protocol ObjectDirective { - var object: ConfigureObject { get } -} diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift index 5ef21ac..e85ba13 100644 --- a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift @@ -8,6 +8,36 @@ public final class DirectiveLocation { } } +// MARK: - Query + +// MARK: - Mutation + +// MARK: - Subscription + +// MARK: - Field + +public struct FieldDirectiveLocation { + public static let field = FieldDirectiveLocation() +} + +public protocol FieldDirective { + func field() +} + +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 @@ -18,8 +48,14 @@ 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, at keyPath: KeyPath) { + convenience init(_ location: ObjectDirectiveLocation) where DirectiveType: ObjectDirective { self.init(location: .object) } } @@ -30,14 +66,33 @@ public struct FieldDefinitionDirectiveLocation { public static let fieldDefinition = FieldDefinitionDirectiveLocation() } +public protocol FieldDefinitionDirective { + func fieldDefinition(field: Field) +} + public extension DirectiveLocation { - convenience init(_ location: FieldDefinitionDirectiveLocation, at keyPath: KeyPath) { + 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 diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 7c449a9..aff04a0 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -7,16 +7,21 @@ public class Field: FieldComponent private var directives: [FieldDefinitionDirective] = [] - override func field(typeProvider: TypeProvider, coders: Coders) throws -> (String, GraphQLField) { - let arguments = try arguments(typeProvider: typeProvider, coders: coders) - applyDirectives(arguments: arguments) + 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: arguments, - resolve: { source, arguments, context, eventLoopGroup, _ in + args: try arguments(typeProvider: typeProvider, coders: coders), + resolve: { source, arguments, context, eventLoopGroup, info in + for directive in typeProvider.directives { + if info.fieldASTs[0].directives.contains(where: { $0.name.value == directive.name }) { + print("Found directive \(directive.name)") + } + } + guard let source = source as? ObjectType else { throw GraphQLError(message: "Expected source type \(ObjectType.self) but got \(type(of: source))") } @@ -44,44 +49,12 @@ public class Field: 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() } diff --git a/Sources/Graphiti/Interface/Interface.swift b/Sources/Graphiti/Interface/Interface.swift index 668f9f9..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 { @@ -72,3 +81,12 @@ public extension Interface { ) } } + +// MARK: Directive + +extension Interface { + func directive(_ 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 f7cfd24..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 { diff --git a/Sources/Graphiti/Query/Query.swift b/Sources/Graphiti/Query/Query.swift index 9b8ec75..5cb9c2b 100644 --- a/Sources/Graphiti/Query/Query.swift +++ b/Sources/Graphiti/Query/Query.swift @@ -22,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 { diff --git a/Sources/Graphiti/Subscription/SubscribeField.swift b/Sources/Graphiti/Subscription/SubscribeField.swift index e6113e4..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, diff --git a/Sources/Graphiti/Subscription/Subscription.swift b/Sources/Graphiti/Subscription/Subscription.swift index 7fb3b09..756a46c 100644 --- a/Sources/Graphiti/Subscription/Subscription.swift +++ b/Sources/Graphiti/Subscription/Subscription.swift @@ -16,7 +16,7 @@ public final class Subscription : Component GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { diff --git a/Sources/Graphiti/Type/Type.swift b/Sources/Graphiti/Type/Type.swift index 2e8ece7..ef37302 100644 --- a/Sources/Graphiti/Type/Type.swift +++ b/Sources/Graphiti/Type/Type.swift @@ -10,6 +10,8 @@ public final class Type : Component : Component GraphQLFieldMap { + func fields(typeProvider: SchemaTypeProvider, coders: Coders) throws -> GraphQLFieldMap { var map: GraphQLFieldMap = [:] for field in fields { @@ -35,16 +36,10 @@ public final class Type : Component GraphQLObjectType { -// var objectType = objectType -// -// for directive in directives { -// var objectConfiguration = ObjectConfiguration(objectType) -// directive.object.configure(&objectConfiguration) -// objectType = GraphQLObjectType(objectConfiguration) -// } -// - return objectType + func applyDirectives() { + for directive in directives { + directive.object(object: self) + } } private init( diff --git a/Tests/GraphitiTests/CounterTests/CounterTests.swift b/Tests/GraphitiTests/CounterTests/CounterTests.swift index df611c1..6909a12 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -164,94 +164,103 @@ class CounterTests: XCTestCase { let name: String } + struct Context { + + } + struct Resolver { let user: User } - struct DelegateField: Decodable, ObjectDirective { + struct DelegateField: Decodable, ObjectDirective, InterfaceDirective { let name: String - var object: ConfigureObject { - ConfigureObject { configuration in -// configuration.fields.append( -// FieldConfiguration( -// name: name, -// description: nil, -// deprecationReason: nil, -// arguments: [], -// resolve: { object in -// { _, _, group in -// group.next().makeCompletedFuture(.success("Yo")) -// } -// } -// ) -// ) - } + 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 { - var fieldDefinition: ConfigureFieldDefinition { - ConfigureFieldDefinition { fieldDefinition in -// let resolve = fieldDefinition.resolve -// -// fieldDefinition.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() -// } -// } -// } + 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() { + print("lowercased") + } } - struct Uppercased: Decodable, FieldDefinitionDirective { - var fieldDefinition: ConfigureFieldDefinition { - ConfigureFieldDefinition { fieldDefinition in -// let resolve = fieldDefinition.resolve -// -// fieldDefinition.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() -// } -// } -// } + 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() { + print("uppercased") + } } - let schema = Schema { + let schema = Schema { Directive(DelegateField.self) { Argument("name", of: String.self, at: \.name) } on: { - DirectiveLocation(.object, at: \.object) -// DirectiveLocation(.interface, at: \.interface) + DirectiveLocation(.object) + DirectiveLocation(.interface) } + #warning("TODO: Implement repeatable directives") // .repeatable() Directive(Lowercased.self) { - DirectiveLocation(.fieldDefinition, at: \.fieldDefinition) + DirectiveLocation(.fieldDefinition) + DirectiveLocation(.field) } Directive(Uppercased.self) { - DirectiveLocation(.fieldDefinition, at: \.fieldDefinition) + DirectiveLocation(.fieldDefinition) + DirectiveLocation(.field) } Type(User.self) { Field("name", of: String.self, at: \.name) - .directive(Uppercased()) - .directive(Lowercased()) +// .directive(Lowercased()) +// .directive(Uppercased()) } .directive(DelegateField(name: "salute")) @@ -264,9 +273,9 @@ class CounterTests: XCTestCase { debugPrint(schema) let result = try schema.execute( - request: "query { user { name } }", + request: "query { user { name @lowercased } }", resolver: Resolver(user: User(name: "Paulo")), - context: (), + context: Context(), on: group ).wait() From 50f4ae95b15d217454d4b8f1ef189be7ade129b3 Mon Sep 17 00:00:00 2001 From: Paulo Faria <5126193+paulofaria@users.noreply.github.com> Date: Wed, 3 Nov 2021 15:32:16 -0300 Subject: [PATCH 19/19] feature: Improve directive support --- Package.resolved | 9 ---- Package.swift | 3 +- Sources/Graphiti/Argument/Argument.swift | 10 +--- .../Graphiti/Argument/ArgumentComponent.swift | 8 --- Sources/Graphiti/Component/Component.swift | 10 ---- Sources/Graphiti/Directive/Directive.swift | 6 ++- .../DirectiveLocation/DirectiveLocation.swift | 2 +- Sources/Graphiti/Enum/Enum.swift | 54 +++++++++++-------- Sources/Graphiti/Field/Field/Field.swift | 34 +++++++----- .../Graphiti/Field/Field/FieldComponent.swift | 10 +--- Sources/Graphiti/Schema/Schema.swift | 2 +- .../Graphiti/Schema/SchemaTypeProvider.swift | 2 +- .../Graphiti/Value/EnumValueComponent.swift | 15 ++++++ Sources/Graphiti/Value/Value.swift | 31 +++++------ Sources/Graphiti/Value/ValueBuilder.swift | 6 +-- .../CounterTests/CounterTests.swift | 30 +++++++++-- 16 files changed, 125 insertions(+), 107 deletions(-) create mode 100644 Sources/Graphiti/Value/EnumValueComponent.swift diff --git a/Package.resolved b/Package.resolved index e20b1a1..b7ba3d2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "GraphQL", - "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", - "state": { - "branch": "feature/async-await", - "revision": "db8a7fc3a4c543cc2eb3bb4139a16b7e0b43fd13", - "version": null - } - }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections", diff --git a/Package.swift b/Package.swift index 16dbe46..0d1a48b 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,8 @@ let package = Package( ], dependencies: [ // .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(url: "https://github.com/GraphQLSwift/GraphQL.git", .branch("feature/async-await")), + .package(path: "../GraphQL") ], targets: [ .target(name: "Graphiti", dependencies: ["GraphQL"]), diff --git a/Sources/Graphiti/Argument/Argument.swift b/Sources/Graphiti/Argument/Argument.swift index a186410..45af548 100644 --- a/Sources/Graphiti/Argument/Argument.swift +++ b/Sources/Graphiti/Argument/Argument.swift @@ -18,18 +18,10 @@ public class Argument : ArgumentCompone self.name = name super.init() } - - 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 Argument { diff --git a/Sources/Graphiti/Argument/ArgumentComponent.swift b/Sources/Graphiti/Argument/ArgumentComponent.swift index cc20f05..21c039e 100644 --- a/Sources/Graphiti/Argument/ArgumentComponent.swift +++ b/Sources/Graphiti/Argument/ArgumentComponent.swift @@ -9,14 +9,6 @@ public class ArgumentComponent: ExpressibleByStringLi fatalError() } - public required init(unicodeScalarLiteral string: String) { - self.description = string - } - - public required init(extendedGraphemeClusterLiteral string: String) { - self.description = string - } - public required init(stringLiteral string: StringLiteralType) { self.description = string } diff --git a/Sources/Graphiti/Component/Component.swift b/Sources/Graphiti/Component/Component.swift index 2f87d62..e2bfdff 100644 --- a/Sources/Graphiti/Component/Component.swift +++ b/Sources/Graphiti/Component/Component.swift @@ -8,16 +8,6 @@ open class Component: ExpressibleByStringLiteral { self.name = name } - public required init(unicodeScalarLiteral string: String) { - self.name = "" - self.description = string - } - - public required init(extendedGraphemeClusterLiteral string: String) { - self.name = "" - self.description = string - } - public required init(stringLiteral string: StringLiteralType) { self.name = "" self.description = string diff --git a/Sources/Graphiti/Directive/Directive.swift b/Sources/Graphiti/Directive/Directive.swift index 6eff39c..2c6f211 100644 --- a/Sources/Graphiti/Directive/Directive.swift +++ b/Sources/Graphiti/Directive/Directive.swift @@ -20,7 +20,11 @@ public final class Directive: Component Any { + try coders.decoder.decode(DirectiveType.self, from: map) } func arguments(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLArgumentConfigMap { diff --git a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift index e85ba13..675ce76 100644 --- a/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift +++ b/Sources/Graphiti/DirectiveLocation/DirectiveLocation.swift @@ -21,7 +21,7 @@ public struct FieldDirectiveLocation { } public protocol FieldDirective { - func field() + func field(resolve: @escaping AsyncResolve) -> AsyncResolve } public extension DirectiveLocation { diff --git a/Sources/Graphiti/Enum/Enum.swift b/Sources/Graphiti/Enum/Enum.swift index b40b4a2..a1e9a0d 100644 --- a/Sources/Graphiti/Enum/Enum.swift +++ b/Sources/Graphiti/Enum/Enum.swift @@ -1,51 +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(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 Enum { convenience init( _ type: EnumType.Type, as name: String? = nil, - @ValueBuilder _ values: () -> Value + @ValueBuilder _ values: () -> EnumValueComponent ) { self.init(type: type, name: name, values: [values()]) } @@ -53,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 aff04a0..57ef649 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -16,9 +16,27 @@ public class Field: FieldComponent: FieldComponent: FieldComponent: ExpressibleByStringLiteral { } init() {} - - public required init(unicodeScalarLiteral string: String) { - self.description = string - } - - public required init(extendedGraphemeClusterLiteral string: String) { - self.description = string - } - + public required init(stringLiteral string: StringLiteralType) { self.description = string } diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index 8984454..f2dd678 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -26,7 +26,7 @@ public final class Schema { mutation: typeProvider.mutation, subscription: typeProvider.subscription, types: typeProvider.types, - directives: typeProvider.directives + directives: typeProvider.directives.map(\.0) ) } } diff --git a/Sources/Graphiti/Schema/SchemaTypeProvider.swift b/Sources/Graphiti/Schema/SchemaTypeProvider.swift index 5b32d26..99a47a1 100644 --- a/Sources/Graphiti/Schema/SchemaTypeProvider.swift +++ b/Sources/Graphiti/Schema/SchemaTypeProvider.swift @@ -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/Value/EnumValueComponent.swift b/Sources/Graphiti/Value/EnumValueComponent.swift new file mode 100644 index 0000000..34b9b1b --- /dev/null +++ b/Sources/Graphiti/Value/EnumValueComponent.swift @@ -0,0 +1,15 @@ +import GraphQL + +public class EnumValueComponent: 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 05edcdb..e982cb3 100644 --- a/Sources/Graphiti/Value/Value.swift +++ b/Sources/Graphiti/Value/Value.swift @@ -1,28 +1,29 @@ +import GraphQL + #warning("TODO: Rename to EnumValue") -public final class Value: ExpressibleByStringLiteral where EnumType.RawValue == String { +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(unicodeScalarLiteral string: String) { - self.value = EnumType(rawValue: "")! - self.description = string - } - - public required init(extendedGraphemeClusterLiteral string: String) { - self.value = EnumType(rawValue: "")! - self.description = string - } - + public required init(stringLiteral string: StringLiteralType) { - self.value = EnumType(rawValue: "")! - self.description = string + fatalError("init(stringLiteral:) has not been implemented") } } 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 index 6909a12..ff0d60d 100644 --- a/Tests/GraphitiTests/CounterTests/CounterTests.swift +++ b/Tests/GraphitiTests/CounterTests/CounterTests.swift @@ -210,8 +210,18 @@ class CounterTests: XCTestCase { } } - func field() { - print("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() + } + } + } } } @@ -232,8 +242,18 @@ class CounterTests: XCTestCase { } } - func field() { - print("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() + } + } + } } } @@ -273,7 +293,7 @@ class CounterTests: XCTestCase { debugPrint(schema) let result = try schema.execute( - request: "query { user { name @lowercased } }", + request: "query { user { name @lowercased @uppercased } }", resolver: Resolver(user: User(name: "Paulo")), context: Context(), on: group