├── .gitignore ├── Tests ├── sourcery.yml ├── ArtemisTests │ ├── Utilities │ │ ├── Utilities.swift │ │ └── Schema+Defaults.swift │ ├── Other │ │ ├── MiscellaneousTests.swift │ │ └── ConditionalTests.swift │ ├── TypeTestCases.swift │ ├── TestInterface1 │ │ └── TestInterface1_Int_TypeTests.swift │ ├── TestInterface2 │ │ └── TestInterface2_Int_TypeTests.swift │ ├── TestInterface3 │ │ └── TestInterface3_Int_TypeTests.swift │ ├── TestInterface4 │ │ └── TestInterface4_Int_TypeTests.swift │ └── TestInterface5 │ │ └── TestInterface5_Int_TypeTests.swift └── Full.xctestplan ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Artemis.xcscheme ├── .github └── workflows │ └── swift.yml ├── Package.swift ├── Sources └── Artemis │ ├── 1. Describing │ ├── Schema.swift │ ├── Array+Schema.swift │ ├── Optional+Schema.swift │ ├── Scalar.swift │ ├── Field.swift │ └── Object.swift │ ├── 2. Selecting │ ├── _SelectionSet.swift │ ├── _SelectionInputOutput.swift │ ├── Fragment.swift │ ├── _Selection.swift │ └── _ArgumentEncoder.swift │ └── 3. Handling │ ├── HTTPNetworkingDelegate.swift │ ├── Partial+I1.swift │ ├── Partial+I2.swift │ ├── Partial+I3.swift │ ├── Partial+I4.swift │ ├── Partial+I5.swift │ ├── Partial.swift │ ├── _Operation.swift │ └── Client.swift ├── LICENSE ├── README.md ├── .swiftlint.yml └── GettingStarted.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | xcschememanagement.plist 7 | -------------------------------------------------------------------------------- /Tests/sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - ArtemisTests/TypeTestCases.swift 3 | templates: 4 | - ArtemisTests/TypeTest.stencil 5 | output: ArtemisTests 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/Utilities/Utilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array { 4 | subscript(safe index: Int) -> Element? { 5 | self.indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: maxim-lobanov/setup-xcode@v1 10 | with: 11 | xcode-version: latest 12 | - uses: actions/checkout@v2 13 | - name: Build 14 | run: swift build -v 15 | - name: Run tests 16 | run: swift test -v 17 | - name: Run SwiftLint 18 | run: swiftlint lint --reporter github-actions-logging 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Artemis", 7 | platforms: [ 8 | .macOS(.v10_10), .iOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "Artemis", targets: ["Artemis"]) 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target(name: "Artemis", dependencies: []), 16 | .testTarget(name: "ArtemisTests", dependencies: ["Artemis"]) 17 | ], 18 | swiftLanguageVersions: [.v5] 19 | ) 20 | -------------------------------------------------------------------------------- /Tests/Full.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "D1B7FF02-F3E4-46A8-A172-6BAF89AD994D", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "parallelizable" : true, 17 | "target" : { 18 | "containerPath" : "container:", 19 | "identifier" : "ArtemisTests", 20 | "name" : "ArtemisTests" 21 | } 22 | } 23 | ], 24 | "version" : 1 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Schema.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that represents a GraphQL schema that declares the API's 'query' and (optionally) 'mutation' types. 5 | */ 6 | public protocol Schema { 7 | /// The type to use for queries on this schema. 8 | associatedtype QueryType: Object 9 | /// The type to use for mutations on this schema. Leave as `Never` if this schema does not support mutations. 10 | associatedtype MutationType = Never 11 | 12 | /// The API's query type whose keypaths are used for field selection on query operations. 13 | static var query: QueryType { get } 14 | /// The API's mutation type whose keypaths are used for field selection on mutation operations. 15 | static var mutation: MutationType { get } 16 | } 17 | 18 | extension Schema where MutationType == Never { 19 | /// The API's mutation type whose keypaths are used for field selection on mutation operations. 20 | public static var mutation: MutationType { fatalError("This should not be possible") } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Aaron Bosnjak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Sources/Artemis/2. Selecting/_SelectionSet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that holds the generic values for a set of one or more selected fields on a type. 5 | 6 | For example, given a selection set like: 7 | ``` 8 | user { 9 | name 10 | age 11 | } 12 | ``` 13 | The 'selection set' is an object that represents 'the selection of the 'name' and 'age' fields on a 'user' type'. 14 | */ 15 | public class _SelectionSet { 16 | var items: [_AnySelection] 17 | var rendered: String 18 | var resultBuilder: ([String: Any]) throws -> Result 19 | 20 | init(items: [_AnySelection], rendered: String, resultBuilder: @escaping ([String: Any]) throws -> Result) { 21 | self.items = items 22 | self.rendered = rendered 23 | self.resultBuilder = resultBuilder 24 | } 25 | 26 | func createResult(from: [String: Any]) throws -> Result { 27 | try self.resultBuilder(from) 28 | } 29 | 30 | var renderedFragmentDeclarations: [String] { 31 | return self.items.flatMap { $0.renderedFragmentDeclarations } 32 | } 33 | 34 | var error: GraphQLError? { 35 | return self.items.compactMap { $0.error }.first 36 | } 37 | 38 | func render() -> String { 39 | return rendered 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Array+Schema.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array: _AnyObject where Element: _AnyObject { 4 | public static var _schemaName: String { Element._schemaName } 5 | } 6 | 7 | extension Array: Object where Element: Object { 8 | public typealias SubSchema = Element.SubSchema 9 | public typealias ImplementedInterfaces = Element.ImplementedInterfaces 10 | 11 | public static var implements: ImplementedInterfaces { Element.implements } 12 | } 13 | 14 | extension Array: Input where Element: Input { } 15 | 16 | extension Array: _SelectionOutput where Element: _SelectionOutput { 17 | public typealias Result = [Element.Result] 18 | 19 | public static func createUnsafeResult(from object: Any, key: String) throws -> R { 20 | guard R.self == Result.self else { throw GraphQLError.invalidOperation } 21 | guard let resultArray = object as? [Any] else { throw GraphQLError.arrayParseFailure(operation: key) } 22 | let mappedArray: [Element.Result] = try resultArray.map { try Element.createUnsafeResult(from: $0, key: key) } 23 | guard let returnedArray = mappedArray as? R else { throw GraphQLError.arrayParseFailure(operation: key) } 24 | return returnedArray 25 | } 26 | 27 | public static var `default`: [Element] { [] } 28 | } 29 | 30 | extension Array: _SelectionInput where Element: _SelectionInput { 31 | public func render() -> String { 32 | return "[\(self.map { $0.render() }.joined(separator: ","))]" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Artemis/2. Selecting/_SelectionInputOutput.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that can be the value of a field selectable for an operation. 5 | 6 | 'Output' types are, in GraphQL terms, 'objects', 'interfaces', 'unions', 'scalars', or 'enums'. Types conforming to 7 | this protocol (done by conforming to the protocol for one of the aforementioned protocols) are able to be used as the 8 | 'return value' for a field selected in an operation (query or mutation). 9 | */ 10 | public protocol _SelectionOutput { 11 | /// The type that is returned in a response when a field with this type is selected. 12 | associatedtype Result = Partial 13 | /// Attempts to create this output's `Result` type from the given object. 14 | static func createUnsafeResult(from: Any, key: String) throws -> Result 15 | 16 | /// A default, 'throwaway' value that can be used to build internal, unused instances of the type. 17 | static var `default`: Self { get } 18 | } 19 | 20 | /** 21 | A type that can be used as the input for an argument to a field. 22 | 23 | 'Input' types are, in GraphQL terms, 'input objects', 'scalars', or 'enums'. Types conforming to this protocol (done by 24 | conforming to the protocol for one of the aforementioned protocols) are able to be used as arguments on a field. 25 | */ 26 | public protocol _SelectionInput { 27 | /// Renders the instance for use in a GraphQL query. 28 | func render() -> String 29 | } 30 | 31 | /** 32 | A type-erased reference to a selection (either `_SelectionSet`, `_FragmentSelection`, or `_Selection`) that allows them 33 | to be put into arrays/individually called for their 'render' strings to build queries. 34 | */ 35 | struct _AnySelection { 36 | var items: [_AnySelection] 37 | var renderedFragmentDeclarations: [String] 38 | var error: GraphQLError? 39 | var render: () -> String 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/HTTPNetworkingDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A networking delegate for a 'client' object that sends GraphQL documents over HTTP to a specific endpoint. 5 | */ 6 | public class HTTPNetworkingDelegate: NetworkDelegate { 7 | public enum Method { 8 | case get 9 | case post 10 | } 11 | let endpoint: URL 12 | let method: Method 13 | 14 | public init(endpoint: URL, method: Method = .post) { 15 | self.endpoint = endpoint 16 | self.method = method 17 | } 18 | 19 | public func send(document: String, completion: @escaping (Result) -> Void) { 20 | var request: URLRequest 21 | 22 | switch self.method { 23 | case .post: 24 | request = URLRequest(url: self.endpoint) 25 | request.httpMethod = "POST" 26 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 27 | 28 | let bodyDict = ["query": document] 29 | guard let httpBody = try? JSONSerialization.data(withJSONObject: bodyDict, options: []) else { 30 | completion(.failure(GraphQLError.invalidRequest(reasons: ["Couldn't serialize to HTTP body"]))) 31 | return 32 | } 33 | request.httpBody = httpBody 34 | case .get: 35 | var urlComps = URLComponents(url: self.endpoint, resolvingAgainstBaseURL: false) 36 | urlComps?.queryItems = [URLQueryItem(name: "query", value: document)] 37 | guard let url = urlComps?.url else { 38 | completion(.failure(GraphQLError.invalidRequest(reasons: ["Couldn't create URL"]))) 39 | return 40 | } 41 | request = URLRequest(url: url) 42 | request.httpMethod = "GET" 43 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 44 | } 45 | 46 | URLSession.shared.dataTask(with: request) { data, response, error in 47 | guard let data = data else { 48 | completion(.failure(error ?? GraphQLError.invalidOperation)) 49 | return 50 | } 51 | completion(.success(data)) 52 | } 53 | .resume() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Optional+Schema.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Optional: _SelectionOutput where Wrapped: _SelectionOutput { 4 | public typealias Result = Wrapped.Result 5 | public static func createUnsafeResult(from: Any, key: String) throws -> Result { 6 | return try Wrapped.createUnsafeResult(from: from, key: key) 7 | } 8 | 9 | public static var `default`: Wrapped? { nil } 10 | } 11 | extension Optional: _SelectionInput where Wrapped: _SelectionInput { 12 | public func render() -> String { 13 | switch self { 14 | case .some(let wrapped): return wrapped.render() 15 | case .none: return "null" 16 | } 17 | } 18 | } 19 | 20 | extension Optional: _AnyObject where Wrapped: _AnyObject { 21 | public static var _schemaName: String { Wrapped._schemaName } 22 | } 23 | 24 | extension Optional: Object where Wrapped: Object { 25 | public typealias SubSchema = Wrapped.SubSchema 26 | public typealias ImplementedInterfaces = Wrapped.ImplementedInterfaces 27 | 28 | public static var implements: ImplementedInterfaces { Wrapped.implements } 29 | 30 | public init() { 31 | self = nil 32 | } 33 | } 34 | 35 | extension Optional: Input where Wrapped: Input { } 36 | 37 | extension Optional: RawRepresentable where Wrapped: RawRepresentable, Wrapped.RawValue == String { 38 | public init?(rawValue: String) { 39 | if let wrapped = Wrapped(rawValue: rawValue) { 40 | self = .some(wrapped) 41 | } 42 | self = .none 43 | } 44 | 45 | public var rawValue: String { 46 | switch self { 47 | case .some(let wrapped): return wrapped.rawValue 48 | case .none: return "" 49 | } 50 | } 51 | } 52 | 53 | extension Optional: CaseIterable where Wrapped: Enum { 54 | public typealias AllCases = [Self] 55 | 56 | public static var allCases: AllCases { 57 | return Wrapped.allCases.map { $0 } 58 | } 59 | } 60 | 61 | extension Optional: Enum where Wrapped: Enum { } 62 | -------------------------------------------------------------------------------- /Sources/Artemis/2. Selecting/Fragment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An object representing a GraphQL 'fragment' that can be added to a sub-selection. 5 | */ 6 | public struct Fragment where T.SubSchema == T { 7 | /// The name given to this fragment to identify it. 8 | public let name: String 9 | var selection: _FragmentSelection 10 | 11 | /** 12 | Creates a new frament usable in a sub-selection with the given name, on the given type, selecting the properties 13 | in the given sub-selection function builder result. 14 | */ 15 | public init(_ name: String, on: T.Type, @_SelectionSetBuilder selection: (_Selector) -> _SelectionSet) { 16 | self.name = name 17 | let underlying = selection(_Selector()) 18 | self.selection = _FragmentSelection( 19 | underlying: underlying, 20 | fragmentDeclaration: "fragment \(name) on \(String(describing: T.self)){\(underlying.render())}" 21 | ) 22 | } 23 | 24 | /** 25 | Creates a new frament usable in a sub-selection with the given name, on the given type, selecting the properties 26 | in the given sub-selection function builder result. 27 | */ 28 | public init(_ name: String, on: T.Type, @_SelectionSetBuilder selection: () -> _SelectionSet) { 29 | self.init(name, on: on, selection: { _ in return selection() }) 30 | } 31 | } 32 | 33 | struct _FragmentSelection { 34 | let underlying: _SelectionSet 35 | let erased: _AnySelection 36 | 37 | fileprivate init(underlying: _SelectionSet, fragmentDeclaration: String) { 38 | self.underlying = underlying 39 | self.erased = _AnySelection( 40 | items: underlying.items, 41 | renderedFragmentDeclarations: underlying.renderedFragmentDeclarations + [fragmentDeclaration], 42 | error: underlying.error, 43 | render: underlying.render 44 | ) 45 | } 46 | 47 | func createResult(from dict: [String: Any]) throws -> Result { 48 | try underlying.createResult(from: dict) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Artemis/2. Selecting/_Selection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A type that adds a field to a selection set. 5 | 6 | Instances of this type are created inside an operation's selection set to specify the fields that are being queried 7 | for on the operation. 8 | 9 | - `T`: The GraphQL object type that we are selecting fields on. For example, a `User` type. 10 | - `Result`: The type returned to the response as a result of selecting this field. 11 | */ 12 | public class _Selection { 13 | let key: String 14 | let createResultClosure: (Any) throws -> Result 15 | 16 | var erased: _AnySelection 17 | 18 | internal init( 19 | key: String, 20 | alias: String?, 21 | arguments: [Argument], 22 | renderedSelectionSet: String?, 23 | createResult: @escaping (Any) throws -> Result, 24 | items: [_AnySelection], 25 | renderedFragmentDeclarations: [String], 26 | error: GraphQLError? 27 | ) { 28 | self.key = alias ?? key 29 | self.createResultClosure = createResult 30 | 31 | let fragmentDeclarations = renderedFragmentDeclarations + items.flatMap { $0.renderedFragmentDeclarations } 32 | self.erased = _AnySelection( 33 | items: items, 34 | renderedFragmentDeclarations: fragmentDeclarations, 35 | error: error, 36 | render: { 37 | var name = key 38 | if let alias = alias { 39 | name = "\(alias):\(key)" 40 | } 41 | var selectionSet = "" 42 | if let renderedSelectionSet = renderedSelectionSet { 43 | selectionSet = "{\(renderedSelectionSet)}" 44 | } 45 | var renderedArgs = arguments 46 | .map { "\($0.name):\($0.value)" } 47 | .joined(separator: ",") 48 | renderedArgs = renderedArgs.isEmpty ? renderedArgs : "(\(renderedArgs))" 49 | 50 | return "\(name)\(renderedArgs)\(selectionSet)" 51 | } 52 | ) 53 | } 54 | 55 | func createResult(from dict: [String: Any]) throws -> Result { 56 | try createResultClosure(dict[self.key] as Any) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Scalar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol that identifies a type as representing a GraphQL 'scalar'. 5 | 6 | 'Scalars' are base types like `String`, `Int`, or `Bool` that can be used as the leaves for an operation. 7 | */ 8 | public protocol Scalar: _SelectionOutput, _SelectionInput { 9 | associatedtype Result = Self 10 | } 11 | 12 | // swiftlint:disable missing_docs 13 | extension Scalar { 14 | public static func createUnsafeResult(from object: Any, key: String) throws -> Result { 15 | guard let returnValue = object as? Result else { throw GraphQLError.singleItemParseFailure(operation: key) } 16 | return returnValue 17 | } 18 | } 19 | // swiftlint:enable missing_docs 20 | 21 | // MARK: Standard type Scalar conformance 22 | 23 | extension Array: Scalar where Element: Scalar { } 24 | 25 | extension Optional: Scalar where Wrapped: Scalar { } 26 | 27 | /** 28 | An alias for `String` that can be used with GraphQL schema for clarity. 29 | */ 30 | public typealias ID = String 31 | 32 | extension String: Scalar { 33 | public typealias Result = String 34 | public typealias Value = String 35 | public func render() -> String { 36 | return "\"\(self)\"" 37 | } 38 | public static var `default`: String { "" } 39 | } 40 | extension Int: Scalar { 41 | public typealias Result = Int 42 | public typealias Value = Int 43 | public func render() -> String { 44 | return String(describing: self) 45 | } 46 | public static var `default`: Int { 0 } 47 | } 48 | extension Float: Scalar { 49 | public typealias Result = Float 50 | public typealias Value = Float 51 | public func render() -> String { 52 | return String(describing: self) 53 | } 54 | public static var `default`: Float { 0 } 55 | 56 | public static func createUnsafeResult(from object: Any, key: String) throws -> Result { 57 | guard let returnValue = object as? Double else { throw GraphQLError.singleItemParseFailure(operation: key) } 58 | return Float(returnValue) 59 | } 60 | } 61 | extension Double: Scalar { 62 | public typealias Result = Double 63 | public typealias Value = Double 64 | public func render() -> String { 65 | return String(describing: self) 66 | } 67 | public static var `default`: Double { 0 } 68 | } 69 | extension Bool: Scalar { 70 | public typealias Result = Bool 71 | public typealias Value = Bool 72 | public func render() -> String { 73 | return String(describing: self) 74 | } 75 | public static var `default`: Bool { false } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial+I1.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable discouraged_optional_collection missing_docs 4 | extension Partial where T: Object, T.ImplementedInterfaces.I1: Interface { 5 | public typealias I1 = T.ImplementedInterfaces.I1 6 | } 7 | 8 | // MARK: Fetching Scalar & [Scalar] 9 | 10 | extension Partial where T: Object, T.ImplementedInterfaces.I1: Interface { 11 | public subscript( 12 | dynamicMember keyPath: KeyPath 13 | ) -> Value.Result? { 14 | return try? I1.key(forPath: keyPath) 15 | .map { self.values[$0] as Any } 16 | .map { try Value.createUnsafeResult(from: $0, key: "") } 17 | } 18 | 19 | public subscript( 20 | dynamicMember keyPath: KeyPath 21 | ) -> Value.Result? { 22 | return try? I1.key(forPath: keyPath) 23 | .map { self.values[$0] as Any } 24 | .map { try Value.createUnsafeResult(from: $0, key: "") } 25 | } 26 | } 27 | 28 | // MARK: Fetching Object & [Object] 29 | 30 | extension Partial where T: Object, T.ImplementedInterfaces.I1: Interface { 31 | public subscript( 32 | dynamicMember keyPath: KeyPath 33 | ) -> Partial? { 34 | return I1.key(forPath: keyPath) 35 | .map { self.values[$0] as? [String: Any] ?? [:] } 36 | .map { Partial(values: $0) } 37 | } 38 | 39 | public subscript( 40 | dynamicMember keyPath: KeyPath 41 | ) -> Partial? { 42 | return I1.key(forPath: keyPath) 43 | .map { self.values[$0] as? [String: Any] ?? [:] } 44 | .map { Partial(values: $0) } 45 | } 46 | 47 | public subscript( 48 | dynamicMember keyPath: KeyPath 49 | ) -> [Partial]? { 50 | return I1.key(forPath: keyPath) 51 | .map { self.values[$0] as? [[String: Any]] ?? [] } 52 | .map { $0.map { Partial(values: $0) } } 53 | } 54 | 55 | public subscript( 56 | dynamicMember keyPath: KeyPath 57 | ) -> [Partial]? { 58 | return I1.key(forPath: keyPath) 59 | .map { self.values[$0] as? [[String: Any]] ?? [] } 60 | .map { $0.map { Partial(values: $0) } } 61 | } 62 | } 63 | 64 | // MARK: Fetching with an alias 65 | 66 | extension Partial where T: Object, T.ImplementedInterfaces.I1: Interface { 67 | public subscript( 68 | dynamicMember keyPath: KeyPath 69 | ) -> Getter { 70 | return Getter(lookup: { self.values[$0] }) 71 | } 72 | 73 | public subscript( 74 | dynamicMember keyPath: KeyPath 75 | ) -> Getter { 76 | return Getter(lookup: { self.values[$0] }) 77 | } 78 | } 79 | // swiftlint:enable discouraged_optional_collection missing_docs 80 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial+I2.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable discouraged_optional_collection missing_docs 4 | extension Partial where T: Object, T.ImplementedInterfaces.I2: Interface { 5 | public typealias I2 = T.ImplementedInterfaces.I2 6 | } 7 | 8 | // MARK: Fetching Scalar & [Scalar] 9 | 10 | extension Partial where T: Object, T.ImplementedInterfaces.I2: Interface { 11 | public subscript( 12 | dynamicMember keyPath: KeyPath 13 | ) -> Value.Result? { 14 | return try? I2.key(forPath: keyPath) 15 | .map { self.values[$0] as Any } 16 | .map { try Value.createUnsafeResult(from: $0, key: "") } 17 | } 18 | 19 | public subscript( 20 | dynamicMember keyPath: KeyPath 21 | ) -> Value.Result? { 22 | return try? I2.key(forPath: keyPath) 23 | .map { self.values[$0] as Any } 24 | .map { try Value.createUnsafeResult(from: $0, key: "") } 25 | } 26 | } 27 | 28 | // MARK: Fetching Object & [Object] 29 | 30 | extension Partial where T: Object, T.ImplementedInterfaces.I2: Interface { 31 | public subscript( 32 | dynamicMember keyPath: KeyPath 33 | ) -> Partial? { 34 | return I2.key(forPath: keyPath) 35 | .map { self.values[$0] as? [String: Any] ?? [:] } 36 | .map { Partial(values: $0) } 37 | } 38 | 39 | public subscript( 40 | dynamicMember keyPath: KeyPath 41 | ) -> Partial? { 42 | return I2.key(forPath: keyPath) 43 | .map { self.values[$0] as? [String: Any] ?? [:] } 44 | .map { Partial(values: $0) } 45 | } 46 | 47 | public subscript( 48 | dynamicMember keyPath: KeyPath 49 | ) -> [Partial]? { 50 | return I2.key(forPath: keyPath) 51 | .map { self.values[$0] as? [[String: Any]] ?? [] } 52 | .map { $0.map { Partial(values: $0) } } 53 | } 54 | 55 | public subscript( 56 | dynamicMember keyPath: KeyPath 57 | ) -> [Partial]? { 58 | return I2.key(forPath: keyPath) 59 | .map { self.values[$0] as? [[String: Any]] ?? [] } 60 | .map { $0.map { Partial(values: $0) } } 61 | } 62 | } 63 | 64 | // MARK: Fetching with an alias 65 | 66 | extension Partial where T: Object, T.ImplementedInterfaces.I2: Interface { 67 | public subscript( 68 | dynamicMember keyPath: KeyPath 69 | ) -> Getter { 70 | return Getter(lookup: { self.values[$0] }) 71 | } 72 | 73 | public subscript( 74 | dynamicMember keyPath: KeyPath 75 | ) -> Getter { 76 | return Getter(lookup: { self.values[$0] }) 77 | } 78 | } 79 | // swiftlint:enable discouraged_optional_collection missing_docs 80 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial+I3.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable discouraged_optional_collection missing_docs 4 | extension Partial where T: Object, T.ImplementedInterfaces.I3: Interface { 5 | public typealias I3 = T.ImplementedInterfaces.I3 6 | } 7 | 8 | // MARK: Fetching Scalar & [Scalar] 9 | 10 | extension Partial where T: Object, T.ImplementedInterfaces.I3: Interface { 11 | public subscript( 12 | dynamicMember keyPath: KeyPath 13 | ) -> Value.Result? { 14 | return try? I3.key(forPath: keyPath) 15 | .map { self.values[$0] as Any } 16 | .map { try Value.createUnsafeResult(from: $0, key: "") } 17 | } 18 | 19 | public subscript( 20 | dynamicMember keyPath: KeyPath 21 | ) -> Value.Result? { 22 | return try? I3.key(forPath: keyPath) 23 | .map { self.values[$0] as Any } 24 | .map { try Value.createUnsafeResult(from: $0, key: "") } 25 | } 26 | } 27 | 28 | // MARK: Fetching Object & [Object] 29 | 30 | extension Partial where T: Object, T.ImplementedInterfaces.I3: Interface { 31 | public subscript( 32 | dynamicMember keyPath: KeyPath 33 | ) -> Partial? { 34 | return I3.key(forPath: keyPath) 35 | .map { self.values[$0] as? [String: Any] ?? [:] } 36 | .map { Partial(values: $0) } 37 | } 38 | 39 | public subscript( 40 | dynamicMember keyPath: KeyPath 41 | ) -> Partial? { 42 | return I3.key(forPath: keyPath) 43 | .map { self.values[$0] as? [String: Any] ?? [:] } 44 | .map { Partial(values: $0) } 45 | } 46 | 47 | public subscript( 48 | dynamicMember keyPath: KeyPath 49 | ) -> [Partial]? { 50 | return I3.key(forPath: keyPath) 51 | .map { self.values[$0] as? [[String: Any]] ?? [] } 52 | .map { $0.map { Partial(values: $0) } } 53 | } 54 | 55 | public subscript( 56 | dynamicMember keyPath: KeyPath 57 | ) -> [Partial]? { 58 | return I3.key(forPath: keyPath) 59 | .map { self.values[$0] as? [[String: Any]] ?? [] } 60 | .map { $0.map { Partial(values: $0) } } 61 | } 62 | } 63 | 64 | // MARK: Fetching with an alias 65 | 66 | extension Partial where T: Object, T.ImplementedInterfaces.I3: Interface { 67 | public subscript( 68 | dynamicMember keyPath: KeyPath 69 | ) -> Getter { 70 | return Getter(lookup: { self.values[$0] }) 71 | } 72 | 73 | public subscript( 74 | dynamicMember keyPath: KeyPath 75 | ) -> Getter { 76 | return Getter(lookup: { self.values[$0] }) 77 | } 78 | } 79 | // swiftlint:enable discouraged_optional_collection missing_docs 80 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial+I4.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable discouraged_optional_collection missing_docs 4 | extension Partial where T: Object, T.ImplementedInterfaces.I4: Interface { 5 | public typealias I4 = T.ImplementedInterfaces.I4 6 | } 7 | 8 | // MARK: Fetching Scalar & [Scalar] 9 | 10 | extension Partial where T: Object, T.ImplementedInterfaces.I4: Interface { 11 | public subscript( 12 | dynamicMember keyPath: KeyPath 13 | ) -> Value.Result? { 14 | return try? I4.key(forPath: keyPath) 15 | .map { self.values[$0] as Any } 16 | .map { try Value.createUnsafeResult(from: $0, key: "") } 17 | } 18 | 19 | public subscript( 20 | dynamicMember keyPath: KeyPath 21 | ) -> Value.Result? { 22 | return try? I4.key(forPath: keyPath) 23 | .map { self.values[$0] as Any } 24 | .map { try Value.createUnsafeResult(from: $0, key: "") } 25 | } 26 | } 27 | 28 | // MARK: Fetching Object & [Object] 29 | 30 | extension Partial where T: Object, T.ImplementedInterfaces.I4: Interface { 31 | public subscript( 32 | dynamicMember keyPath: KeyPath 33 | ) -> Partial? { 34 | return I4.key(forPath: keyPath) 35 | .map { self.values[$0] as? [String: Any] ?? [:] } 36 | .map { Partial(values: $0) } 37 | } 38 | 39 | public subscript( 40 | dynamicMember keyPath: KeyPath 41 | ) -> Partial? { 42 | return I4.key(forPath: keyPath) 43 | .map { self.values[$0] as? [String: Any] ?? [:] } 44 | .map { Partial(values: $0) } 45 | } 46 | 47 | public subscript( 48 | dynamicMember keyPath: KeyPath 49 | ) -> [Partial]? { 50 | return I4.key(forPath: keyPath) 51 | .map { self.values[$0] as? [[String: Any]] ?? [] } 52 | .map { $0.map { Partial(values: $0) } } 53 | } 54 | 55 | public subscript( 56 | dynamicMember keyPath: KeyPath 57 | ) -> [Partial]? { 58 | return I4.key(forPath: keyPath) 59 | .map { self.values[$0] as? [[String: Any]] ?? [] } 60 | .map { $0.map { Partial(values: $0) } } 61 | } 62 | } 63 | 64 | // MARK: Fetching with an alias 65 | 66 | extension Partial where T: Object, T.ImplementedInterfaces.I4: Interface { 67 | public subscript( 68 | dynamicMember keyPath: KeyPath 69 | ) -> Getter { 70 | return Getter(lookup: { self.values[$0] }) 71 | } 72 | 73 | public subscript( 74 | dynamicMember keyPath: KeyPath 75 | ) -> Getter { 76 | return Getter(lookup: { self.values[$0] }) 77 | } 78 | } 79 | // swiftlint:enable discouraged_optional_collection missing_docs 80 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial+I5.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable discouraged_optional_collection missing_docs 4 | extension Partial where T: Object, T.ImplementedInterfaces.I5: Interface { 5 | public typealias I5 = T.ImplementedInterfaces.I5 6 | } 7 | 8 | // MARK: Fetching Scalar & [Scalar] 9 | 10 | extension Partial where T: Object, T.ImplementedInterfaces.I5: Interface { 11 | public subscript( 12 | dynamicMember keyPath: KeyPath 13 | ) -> Value.Result? { 14 | return try? I5.key(forPath: keyPath) 15 | .map { self.values[$0] as Any } 16 | .map { try Value.createUnsafeResult(from: $0, key: "") } 17 | } 18 | 19 | public subscript( 20 | dynamicMember keyPath: KeyPath 21 | ) -> Value.Result? { 22 | return try? I5.key(forPath: keyPath) 23 | .map { self.values[$0] as Any } 24 | .map { try Value.createUnsafeResult(from: $0, key: "") } 25 | } 26 | } 27 | 28 | // MARK: Fetching Object & [Object] 29 | 30 | extension Partial where T: Object, T.ImplementedInterfaces.I5: Interface { 31 | public subscript( 32 | dynamicMember keyPath: KeyPath 33 | ) -> Partial? { 34 | return I5.key(forPath: keyPath) 35 | .map { self.values[$0] as? [String: Any] ?? [:] } 36 | .map { Partial(values: $0) } 37 | } 38 | 39 | public subscript( 40 | dynamicMember keyPath: KeyPath 41 | ) -> Partial? { 42 | return I5.key(forPath: keyPath) 43 | .map { self.values[$0] as? [String: Any] ?? [:] } 44 | .map { Partial(values: $0) } 45 | } 46 | 47 | public subscript( 48 | dynamicMember keyPath: KeyPath 49 | ) -> [Partial]? { 50 | return I5.key(forPath: keyPath) 51 | .map { self.values[$0] as? [[String: Any]] ?? [] } 52 | .map { $0.map { Partial(values: $0) } } 53 | } 54 | 55 | public subscript( 56 | dynamicMember keyPath: KeyPath 57 | ) -> [Partial]? { 58 | return I5.key(forPath: keyPath) 59 | .map { self.values[$0] as? [[String: Any]] ?? [] } 60 | .map { $0.map { Partial(values: $0) } } 61 | } 62 | } 63 | 64 | // MARK: Fetching with an alias 65 | 66 | extension Partial where T: Object, T.ImplementedInterfaces.I5: Interface { 67 | public subscript( 68 | dynamicMember keyPath: KeyPath 69 | ) -> Getter { 70 | return Getter(lookup: { self.values[$0] }) 71 | } 72 | 73 | public subscript( 74 | dynamicMember keyPath: KeyPath 75 | ) -> Getter { 76 | return Getter(lookup: { self.values[$0] }) 77 | } 78 | } 79 | // swiftlint:enable discouraged_optional_collection missing_docs 80 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Artemis.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/Utilities/Schema+Defaults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let testArgs = TestArguments.expectedTestRenderedString 4 | 5 | extension TestArguments { 6 | static let testDefault = TestArguments( 7 | input: .testDefault, 8 | inputs: [.testDefault], 9 | enum: .first, 10 | enums: [.first, .first], 11 | int: 123, 12 | ints: [123, 321], 13 | string: "123", 14 | strings: ["123", "321"], 15 | bool: true, 16 | bools: [true, false], 17 | double: 1.1, 18 | doubles: [1.1, 2.2], 19 | float: 1.1, 20 | floats: [1.1, 2.2] 21 | ) 22 | 23 | static let expectedTestRenderedString: String = { 24 | return "(" + 25 | "input:\(TestInput.expectedTestRenderedString)," + 26 | "inputs:[\(TestInput.expectedTestRenderedString)]," + 27 | "enum:FIRST," + 28 | "enums:[FIRST,FIRST]," + 29 | "int:123," + 30 | "ints:[123,321]," + 31 | "string:\"123\"," + 32 | "strings:[\"123\",\"321\"]," + 33 | "bool:true," + 34 | "bools:[true,false]," + 35 | "double:1.1," + 36 | "doubles:[1.1,2.2]," + 37 | "float:1.1," + 38 | "floats:[1.1,2.2]" + 39 | ")" 40 | }() 41 | } 42 | 43 | extension TestInput { 44 | static let testDefault = TestInput( 45 | input: .testDefault, 46 | inputs: [.testDefault], 47 | enum: .second, 48 | enums: [.second, .second], 49 | int: 234, 50 | ints: [234, 432], 51 | string: "234", 52 | strings: ["234", "432"], 53 | bool: true, 54 | bools: [true, false], 55 | double: 1.11, 56 | doubles: [1.11, 2.22], 57 | float: 1.11, 58 | floats: [1.11, 2.22] 59 | ) 60 | 61 | static let expectedTestRenderedString: String = { 62 | return "{" + 63 | "input:\(TestInput.TestSubInput.expectedTestRenderedString)," + 64 | "inputs:[\(TestInput.TestSubInput.expectedTestRenderedString)]," + 65 | "enum:SECOND," + 66 | "enums:[SECOND,SECOND]," + 67 | "int:234," + 68 | "ints:[234,432]," + 69 | "string:\"234\"," + 70 | "strings:[\"234\",\"432\"]," + 71 | "bool:true," + 72 | "bools:[true,false]," + 73 | "double:1.11," + 74 | "doubles:[1.11,2.22]," + 75 | "float:1.11," + 76 | "floats:[1.11,2.22]" + 77 | "}" 78 | }() 79 | } 80 | 81 | extension TestInput.TestSubInput { 82 | static let testDefault = TestInput.TestSubInput( 83 | enum: .third, 84 | enums: [.third, .third], 85 | int: 345, 86 | ints: [345, 543], 87 | string: "345", 88 | strings: ["345", "543"], 89 | bool: true, 90 | bools: [true, false], 91 | double: 1.111, 92 | doubles: [1.111, 2.222], 93 | float: 1.111, 94 | floats: [1.111, 2.222] 95 | ) 96 | 97 | static let expectedTestRenderedString: String = { 98 | return "{" + 99 | "enum:THIRD," + 100 | "enums:[THIRD,THIRD]," + 101 | "int:345," + 102 | "ints:[345,543]," + 103 | "string:\"345\"," + 104 | "strings:[\"345\",\"543\"]," + 105 | "bool:true," + 106 | "bools:[true,false]," + 107 | "double:1.111," + 108 | "doubles:[1.111,2.222]," + 109 | "float:1.111," + 110 | "floats:[1.111,2.222]" + 111 | "}" 112 | }() 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Field.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A property wrapper that marks a property on an `Object` as a GraphQL field. 5 | 6 | This property wrapper must be applied to properties on an `Object` for its properties to be selectable for an 7 | operation. Failure to add this wrapper results in a runtime error. This property wrapper can only be applied to 8 | `Object` or `Interface` types. 9 | */ 10 | @propertyWrapper 11 | public struct Field { 12 | /// The field's wrapped value. This will not return a real value; it is only used for triggering the caching of the 13 | /// field's string name. 14 | public static subscript( 15 | _enclosingInstance object: OuterSelf, 16 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 17 | storage storageKeyPath: ReferenceWritableKeyPath 18 | ) -> T { 19 | get { 20 | OuterSelf.set(key: object[keyPath: storageKeyPath].key, forPath: wrappedKeyPath) 21 | return object[keyPath: storageKeyPath].throwawayValue() 22 | } 23 | set { } 24 | } 25 | 26 | /// The string name of the field as it should appear in a document. 27 | public let key: String 28 | 29 | /// The wrapped value of the field. This should not be accessible. 30 | @available(*, unavailable, message: "Fields are available on classes conforming to Object, Interface, or Input") 31 | public var wrappedValue: T { 32 | get { fatalError("wrappedValue is unavailable") } 33 | // swiftlint:disable:next unused_setter_value 34 | set { fatalError("wrappedValue is unavailable") } 35 | } 36 | private let throwawayValue: () -> T 37 | } 38 | 39 | extension Field where T == (Value, ArgType.Type) { 40 | /** 41 | Instantiates a new field with the given name. 42 | - parameter key: The string name of the field as it will appear in GraphQL documents. 43 | */ 44 | public init(_ key: String) { 45 | self.key = key 46 | self.throwawayValue = { (.default, ArgType.self) } 47 | } 48 | } 49 | 50 | extension Field where Value: _SelectionOutput, ArgType == NoArguments, T == Value { 51 | /** 52 | Instantiates a new field with the given name. 53 | - parameter key: The string name of the field as it will appear in GraphQL documents. 54 | */ 55 | public init(_ key: String) { 56 | self.key = key 57 | self.throwawayValue = { .default } 58 | } 59 | } 60 | 61 | /** 62 | A protocol that a type whose properties represent arguments for a `Field` must conform to. 63 | */ 64 | public protocol ArgumentsList: Encodable { } 65 | 66 | /// For optimization, instantiated `ArgumentsList` objects are stored here under the string names of their parent 67 | /// `ArgumentsList` types. 68 | private var cachedArgListsForTypes: [String: Any] = [:] 69 | /// Since there's no way to get string names for `KeyPath` instances, we store them here after pulling them from 70 | /// `Argument` instances, under the string names of their parent `ArgumentsList` types. The `AnyKeyPath`s here are paths 71 | /// on the `ArgumentsList`, and the `String` value returned is the string name of the keypath we can use with GraphQL. 72 | private var nameStringsForArgumentKeyPaths: [String: [AnyKeyPath: String]] = [:] 73 | 74 | /** 75 | A type that can be used with a `Field` instance to indicate that the field takes no arguments. If an argument type is 76 | not 77 | */ 78 | public struct NoArguments: ArgumentsList { 79 | public func encode(to encoder: Encoder) throws { } 80 | } 81 | 82 | internal struct Argument { 83 | var occurence: Int = 0 84 | var name: String 85 | var value: String 86 | } 87 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/Other/MiscellaneousTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Artemis 3 | 4 | class MiscellaneousTests: XCTestCase { 5 | func testOperationNameRender() { 6 | let query: _Operation = .query(name: "OperationName") { 7 | $0.int 8 | } 9 | 10 | XCTAssertEqual(query.render(), "query OperationName{int}") 11 | } 12 | 13 | func testClientWithDelegate() { 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "testObject": { 18 | "int": 123, 19 | "bool": true 20 | } 21 | } 22 | } 23 | """.utf8) 24 | let delegate = MockNetworkDelegate(result: .success(response)) 25 | let client = Client(networkDelegate: delegate) 26 | 27 | var result: Result, GraphQLError>? 28 | 29 | client.perform(.query { 30 | $0.testObject { 31 | $0.int 32 | $0.bool 33 | } 34 | }) { res in 35 | result = res 36 | } 37 | 38 | guard let value = try? result?.get() else { 39 | XCTFail() 40 | return 41 | } 42 | XCTAssertEqual(value.values.count, 2) 43 | XCTAssertEqual(value.int, 123) 44 | XCTAssertEqual(value.bool, true) 45 | } 46 | 47 | func testClientWithMock() { 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "testObject": { 52 | "int": 123, 53 | "bool": true 54 | } 55 | } 56 | } 57 | """.utf8) 58 | let client = Client(endpoint: URL(string: "fake.com")!) 59 | 60 | var result: Result, GraphQLError>? 61 | 62 | client.perform(.query { 63 | $0.testObject { 64 | $0.int 65 | $0.bool 66 | } 67 | }, mock: response) { res in 68 | result = res 69 | } 70 | 71 | guard let value = try? result?.get() else { 72 | XCTFail() 73 | return 74 | } 75 | XCTAssertEqual(value.values.count, 2) 76 | XCTAssertEqual(value.int, 123) 77 | XCTAssertEqual(value.bool, true) 78 | } 79 | 80 | func testClientWithError() { 81 | let response = Data(""" 82 | { 83 | "errors": [ 84 | { 85 | "message": "This is an error" 86 | } 87 | ] 88 | } 89 | """.utf8) 90 | let client = Client(endpoint: URL(string: "fake.com")!) 91 | 92 | var result: Result, GraphQLError>? 93 | 94 | client.perform(.query { 95 | $0.testObject { 96 | $0.int 97 | $0.bool 98 | } 99 | }, mock: response) { res in 100 | result = res 101 | } 102 | 103 | switch result { 104 | case .success, .none: 105 | XCTFail("Should have failed") 106 | case .failure(let error): 107 | switch error { 108 | case .invalidRequest(let reasons): 109 | XCTAssertEqual(reasons.count, 1) 110 | XCTAssertEqual(reasons.first, "This is an error") 111 | default: 112 | XCTFail("Should have failed with 'invalidRequest'") 113 | } 114 | } 115 | } 116 | 117 | func testClientWithInvalidJSON() { 118 | let response = Data(""" 119 | ajdf{df..] 120 | """.utf8) 121 | let client = Client(endpoint: URL(string: "fake.com")!) 122 | 123 | var result: Result, GraphQLError>? 124 | 125 | client.perform(.query { 126 | $0.testObject { 127 | $0.int 128 | $0.bool 129 | } 130 | }, mock: response) { res in 131 | result = res 132 | } 133 | 134 | switch result { 135 | case .success, .none: 136 | XCTFail("Should have failed") 137 | case .failure(let error): 138 | switch error { 139 | case .malformattedResponse: break 140 | default: 141 | XCTFail("Should have failed with 'malformattedResponse'") 142 | } 143 | } 144 | } 145 | } 146 | 147 | private class MockNetworkDelegate: NetworkDelegate { 148 | var result: Result 149 | 150 | init(result: Result) { 151 | self.result = result 152 | } 153 | 154 | func send(document: String, completion: @escaping (Result) -> Void) { 155 | completion(self.result) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Artemis 2 | 3 |

4 | Build status 5 | Platform iOS 6 | Swift 5.3 compatible 7 | License: MIT 8 |

9 | 10 | Artemis is a GraphQL library for Swift that lets you interact with a GraphQL backend entirely in Swift - no unsafe queries made of strings, 11 | no `Data` or `[String: Any]` responses you need to parse though manually. Artemis uses various Swift features to keep track of types used 12 | in queries, so this request: 13 | 14 | ```swift 15 | // Artemis // Rendered GraphQL query 16 | .query { query { 17 | $0.country(arguments: .init(code: "CA")) { country(code: "CA") { 18 | $0.name name 19 | $0.continent { continent { 20 | $0.name(alias: "continentName") continentName: name 21 | } } 22 | } } 23 | } } 24 | ``` 25 | 26 | ...is entirely type-checked - we can't add fields not on our schema, put fields in the wrong place, pass invalid arguments, or pass 27 | invalid types for our arguments. 28 | 29 | Performing that query results in a `Partial` object that you can interact with using the same keypaths and type inference 30 | as a normal `Country` instance. Artemis will populate the response object with the fetched data - so this query (and its response) 31 | are handled like this: 32 | 33 | ```swift 34 | let client = Client() 35 | 36 | client.perform(.query { ... }) { result in 37 | switch result { 38 | case .success(let country): 39 | country.name // "Canada" 40 | country.continent?.name(alias: "continentName") // "North America" 41 | country.languages // nil 42 | case .failure(let error): 43 | // error handling 44 | } 45 | } 46 | ``` 47 | 48 | The schema representation in Swift is also very readable and maintains a reasonably close resemblance to the GraphQL schema syntax. 49 | A schema for the above query might look something like this: 50 | 51 | ```swift 52 | // Artemis // Original GraphQL schema 53 | final class Query: Object { type Query { 54 | @Field("country") country(code: String!): Country! 55 | var country: (Country, CountryArgs.Type) } 56 | 57 | struct CountryArgs: ArgumentsList { type Country { 58 | var code: String name: String! 59 | } languages: [String!]! 60 | } continent: Continent! 61 | } 62 | final class Country: Object { 63 | @Field("name") type Continent { 64 | var name: String name: String! 65 | } 66 | @Field("languages") 67 | var languages: [String] 68 | 69 | @Field("continent") 70 | var continent: Continent 71 | } 72 | 73 | final class Continent: Object { 74 | @Field("name") 75 | var name: String 76 | } 77 | 78 | ``` 79 | 80 | Don't let this simple example sell Artemis short, though - it includes full support for fragments, arguments, interfaces, inputs, aliases, 81 | mutations, and multiple query fields. It's also very light (requiring only `Foundation`), so supports iOS, macOS, or anywhere else Swift 82 | and Foundation can run. 83 | 84 | Artemis can be added to any project using Swift Package Manager. 85 | 86 | Once you have it added to a project, you can check out the tutorial on [getting started with Artemis](https://github.com/Saelyria/Artemis/tree/master/GettingStarted.md) 87 | guide to get up and running with making requests. 88 | 89 | ## Contributors 90 | 91 | Aaron Bosnjak (email: aaron.bosnjak707@gmail.com, Twitter: @aaron_bosnjak) 92 | 93 | Artemis is open to contributors! If you have a feature idea or a bug fix, feel free to open a pull request. Issues and feature ideas are tracked on 94 | this [Trello board](https://trello.com/b/iDjeDfov/artemis). 95 | 96 | ## License 97 | 98 | Artemis is available under the MIT license, so do pretty much anything you want with it. As always, see the LICENSE file for more info. 99 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - .swiftpm 3 | - Tests 4 | 5 | disabled_rules: 6 | - array_init 7 | - block_based_kvo 8 | - class_delegate_protocol 9 | - closure_body_length 10 | - conditional_returns_on_newline 11 | - convenience_type 12 | - cyclomatic_complexity 13 | - discouraged_object_literal 14 | - duplicate_enum_cases 15 | - enum_case_associated_values_count 16 | - expiring_todo 17 | - explicit_acl 18 | - explicit_enum_raw_value 19 | - explicit_self 20 | - explicit_top_level_acl 21 | - explicit_type_interface 22 | - extension_access_modifier 23 | - file_header 24 | - file_length 25 | - file_name 26 | - file_types_order 27 | - for_where 28 | - function_body_length 29 | - function_default_parameter_at_end 30 | - function_parameter_count 31 | - ibinspectable_in_extension 32 | - implicit_return 33 | - implicitly_unwrapped_optional 34 | - indentation_width 35 | - large_tuple 36 | - let_var_whitespace 37 | - nesting 38 | - nimble_operator 39 | - no_fallthrough_only 40 | - no_grouping_extension 41 | - notification_center_detachment 42 | - nslocalizedstring_require_bundle 43 | - number_separator 44 | - optional_enum_case_matching 45 | - override_in_extension 46 | - pattern_matching_keywords 47 | - prefer_nimble 48 | - prefixed_toplevel_constant 49 | - prohibited_interface_builder 50 | - quick_discouraged_call 51 | - quick_discouraged_focused_test 52 | - quick_discouraged_pending_test 53 | - redundant_string_enum_value 54 | - required_deinit 55 | - required_enum_case 56 | - sorted_first_last 57 | - sorted_imports 58 | - strict_fileprivate 59 | - switch_case_on_newline 60 | - test_case_accessibility 61 | - trailing_closure 62 | - type_body_length 63 | - type_contents_order 64 | - type_name 65 | - untyped_error_in_catch 66 | - unused_capture_list 67 | - unused_closure_parameter 68 | - unused_control_flow_label 69 | - unused_enumerated 70 | - vertical_whitespace_between_cases 71 | - xct_specific_matcher 72 | - xctfail_message 73 | 74 | opt_in_rules: 75 | - anyobject_protocol 76 | - attributes 77 | - closing_brace 78 | - closure_end_indentation 79 | - closure_parameter_position 80 | - closure_spacing 81 | - collection_alignment 82 | - colon 83 | - comma 84 | - comment_spacing 85 | - compiler_protocol_init 86 | - computed_accessors_order 87 | - contains_over_filter_count 88 | - contains_over_filter_is_empty 89 | - contains_over_first_not_nil 90 | - contains_over_range_nil_comparison 91 | - control_statement 92 | - deployment_target 93 | - discarded_notification_center_observer 94 | - discouraged_direct_init 95 | - discouraged_optional_boolean 96 | - discouraged_optional_collection 97 | - duplicate_imports 98 | - dynamic_inline 99 | - empty_collection_literal 100 | - empty_count 101 | - empty_enum_arguments 102 | - empty_parameters 103 | - empty_parentheses_with_trailing_closure 104 | - empty_string 105 | - empty_xctest_method 106 | - explicit_init 107 | - fallthrough 108 | - fatal_error_message 109 | - file_name_no_space 110 | - first_where 111 | - flatmap_over_map_reduce 112 | - force_cast 113 | - force_try 114 | - force_unwrapping 115 | - generic_type_name 116 | - identical_operands 117 | - identifier_name 118 | - implicit_getter 119 | - inclusive_language 120 | - inert_defer 121 | - is_disjoint 122 | - joined_default_parameter 123 | - last_where 124 | - leading_whitespace 125 | - legacy_cggeometry_functions 126 | - legacy_constant 127 | - legacy_constructor 128 | - legacy_hashing 129 | - legacy_multiple 130 | - legacy_nsgeometry_functions 131 | - legacy_random 132 | - line_length 133 | - literal_expression_end_indentation 134 | - lower_acl_than_parent 135 | - mark 136 | - missing_docs 137 | - modifier_order 138 | - multiline_arguments 139 | - multiline_arguments_brackets 140 | - multiline_function_chains 141 | - multiline_literal_brackets 142 | - multiline_parameters 143 | - multiline_parameters_brackets 144 | - no_extension_access_modifier 145 | - no_space_in_method_call 146 | - nslocalizedstring_key 147 | - nsobject_prefer_isequal 148 | - object_literal 149 | - opening_brace 150 | - operator_usage_whitespace 151 | - operator_whitespace 152 | - orphaned_doc_comment 153 | - overridden_super_call 154 | - prefer_self_type_over_type_of_self 155 | - prefer_zero_over_explicit_init 156 | - private_action 157 | - private_outlet 158 | - private_over_fileprivate 159 | - private_unit_test 160 | - prohibited_super_call 161 | - protocol_property_accessors_order 162 | - raw_value_for_camel_cased_codable_enum 163 | - reduce_boolean 164 | - reduce_into 165 | - redundant_discardable_let 166 | - redundant_nil_coalescing 167 | - redundant_objc_attribute 168 | - redundant_optional_initialization 169 | - redundant_set_access_control 170 | - redundant_type_annotation 171 | - redundant_void_return 172 | - return_arrow_whitespace 173 | - shorthand_operator 174 | - single_test_class 175 | - statement_position 176 | - static_operator 177 | - strong_iboutlet 178 | - superfluous_disable_command 179 | - switch_case_alignment 180 | - syntactic_sugar 181 | - todo 182 | - toggle_bool 183 | - trailing_comma 184 | - trailing_newline 185 | - trailing_semicolon 186 | - trailing_whitespace 187 | - unavailable_function 188 | - unneeded_break_in_switch 189 | - unneeded_parentheses_in_closure_argument 190 | - unowned_variable_capture 191 | - unused_optional_binding 192 | - unused_setter_value 193 | - valid_ibinspectable 194 | - vertical_parameter_alignment 195 | - vertical_parameter_alignment_on_call 196 | - vertical_whitespace 197 | - vertical_whitespace_opening_braces 198 | - void_return 199 | - weak_delegate 200 | - yoda_condition 201 | 202 | analyzer_rules: 203 | - unused_declaration 204 | - unused_import 205 | 206 | identifier_name: 207 | min_length: 2 208 | excluded: 209 | - id 210 | - _schemaName 211 | 212 | multiline_arguments: 213 | only_enforce_after_first_closure_on_first_line: true 214 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Partial.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An object that wraps values related to a wrapped type. 5 | 6 | When using GraphQL, we often don't fetch full representations of model objects - instead, we get 'partial' instances 7 | that only contain some properties. This object is used to be this 'partial' representation of a model object - 8 | `Partial` instances are specialized with a type to represent, and are returned as the result of a GraphQL operation. 9 | 10 | `Partial` instances can be queried for properties related to their underlying represented type with the same property 11 | names as the represented type in a type-safe way using `KeyPath` objects. 12 | */ 13 | @dynamicMemberLookup 14 | public struct Partial { 15 | /// A callable object that retrieves a value under an alias. 16 | public struct Getter { 17 | var lookup: (String) -> Any? 18 | } 19 | 20 | let values: [String: Any] 21 | } 22 | 23 | extension Partial.Getter where Value: Scalar { 24 | /** 25 | Gets the value for the field that was requested under an alias. 26 | - parameter alias: The alias the field was requested under. 27 | */ 28 | public func callAsFunction(alias: String) -> Value.Result? { 29 | return try? Value.createUnsafeResult(from: lookup(alias) as Any, key: "") 30 | } 31 | } 32 | 33 | extension Partial.Getter where Value: Object { 34 | /** 35 | Gets the value for the field that was requested under an alias. 36 | - parameter alias: The alias the field was requested under. 37 | */ 38 | public func callAsFunction(alias: String) -> Partial? { 39 | let valueDict = lookup(alias) as? [String: Any] 40 | return valueDict.map { Partial(values: $0) } 41 | } 42 | } 43 | 44 | // swiftlint:disable discouraged_optional_collection 45 | extension Partial.Getter where Value: Object & Collection, Value.Element: _SelectionOutput { 46 | /** 47 | Gets the value for the field that was requested under an alias. 48 | - parameter alias: The alias the field was requested under. 49 | */ 50 | public func callAsFunction(alias: String) -> [Partial]? { 51 | let valuesArray = lookup(alias) as? [[String: Any]] ?? [] 52 | return valuesArray.map { Partial(values: $0) } 53 | } 54 | } 55 | // swiftlint:enable discouraged_optional_collection 56 | 57 | // MARK: Fetching Scalar & [Scalar] 58 | 59 | // swiftlint:disable discouraged_optional_collection missing_docs 60 | extension Partial where T: Object { 61 | public subscript( 62 | dynamicMember keyPath: KeyPath 63 | ) -> Value.Result? { 64 | return try? T.key(forPath: keyPath) 65 | .map { self.values[$0] as Any } 66 | .map { try Value.createUnsafeResult(from: $0, key: "") } 67 | } 68 | 69 | public subscript( 70 | dynamicMember keyPath: KeyPath 71 | ) -> Value.Result? { 72 | return try? T.key(forPath: keyPath) 73 | .map { self.values[$0] as Any } 74 | .map { try Value.createUnsafeResult(from: $0, key: "") } 75 | } 76 | } 77 | 78 | // MARK: Fetching Object & [Object] 79 | 80 | extension Partial where T: Object { 81 | public subscript( 82 | dynamicMember keyPath: KeyPath 83 | ) -> Partial? { 84 | return T.key(forPath: keyPath) 85 | .map { self.values[$0] as? [String: Any] ?? [:] } 86 | .map { Partial(values: $0) } 87 | } 88 | 89 | public subscript( 90 | dynamicMember keyPath: KeyPath 91 | ) -> Partial? { 92 | return T.key(forPath: keyPath) 93 | .map { self.values[$0] as? [String: Any] ?? [:] } 94 | .map { Partial(values: $0) } 95 | } 96 | 97 | public subscript( 98 | dynamicMember keyPath: KeyPath 99 | ) -> [Partial]? { 100 | return T.key(forPath: keyPath) 101 | .map { self.values[$0] as? [[String: Any]] ?? [] } 102 | .map { $0.map { Partial(values: $0) } } 103 | } 104 | 105 | public subscript( 106 | dynamicMember keyPath: KeyPath 107 | ) -> [Partial]? { 108 | return T.key(forPath: keyPath) 109 | .map { self.values[$0] as? [[String: Any]] ?? [] } 110 | .map { $0.map { Partial(values: $0) } } 111 | } 112 | } 113 | 114 | // MARK: Fetching with an alias 115 | 116 | extension Partial where T: Object { 117 | public subscript( 118 | dynamicMember keyPath: KeyPath 119 | ) -> Getter { 120 | return Getter(lookup: { self.values[$0] }) 121 | } 122 | 123 | public subscript( 124 | dynamicMember keyPath: KeyPath 125 | ) -> Getter { 126 | return Getter(lookup: { self.values[$0] }) 127 | } 128 | } 129 | // swiftlint:enable discouraged_optional_collection missing_docs 130 | 131 | extension Partial: CustomStringConvertible { 132 | public var description: String { 133 | var desc = "Partial<\(String(describing: T.self).split(separator: ".").last ?? "")>(" 134 | let values = self.values 135 | .map { pair -> String in 136 | let (key, value) = pair 137 | var valueDesc: String = "nil" 138 | if let value = value as? CustomStringConvertible { 139 | valueDesc = value.description 140 | } 141 | return "\(key): \(valueDesc)" 142 | } 143 | .joined(separator: ", ") 144 | desc.append("\(values))") 145 | 146 | return desc 147 | } 148 | } 149 | 150 | public enum GraphQLError: Error { 151 | case invalidOperation 152 | case malformattedResponse(reason: String) 153 | case singleItemParseFailure(operation: String) 154 | case arrayParseFailure(operation: String) 155 | /// The type of the associated value of an argument enum case wasn't convertible to a string. 156 | case argumentValueNotConvertible 157 | case invalidRequest(reasons: [String]) 158 | case other(Error) 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Artemis/1. Describing/Object.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A protocol that identifies a type as representing a GraphQL 'object'. 5 | 6 | 'Objects' in GraphQL are any types that have selectable fields of other 'objects' or 'scalars'. This protocol is 7 | conformed to by data types that are meant to represent the various objects of your GraphQL API. 8 | */ 9 | public protocol Object: _SelectionOutput, _AnyObject { 10 | /// The type whose keypaths can be used to construct GraphQL queries. Defaults to `Self`. 11 | associatedtype SubSchema: Object = Self 12 | associatedtype Result = Partial 13 | 14 | associatedtype ImplementedInterfaces: AnyInterfaces = Interfaces 15 | 16 | static var implements: ImplementedInterfaces { get } 17 | 18 | init() 19 | } 20 | // swiftlint:disable missing_docs 21 | extension Object where SubSchema == Self { 22 | public static var `default`: Self { 23 | return self.schema 24 | } 25 | } 26 | 27 | /// For optimization, instantiated `Object.SubSchema` objects are stored here under the string names of their parent 28 | /// `Object` types. 29 | private var cachedSchemasForTypes: [String: Any] = [:] 30 | /// Since there's no way to get string names for `KeyPath` instances, we store them here after pulling them from 31 | /// `Field` instances, under the string names of their parent `Object` types. The `AnyKeyPath`s here are paths on the 32 | /// `Object.SubSchema`, and the `String` value returned is the string name of the keypath we can use with GraphQL. 33 | private var keyStringsForSchemaKeyPaths: [String: [AnyKeyPath: String]] = [:] 34 | 35 | extension Object { 36 | public static var _schemaName: String { String(describing: SubSchema.self) } 37 | 38 | internal static var schema: SubSchema { 39 | let schema: SubSchema 40 | if let cachedSchema = cachedSchemasForTypes[_schemaName] as? SubSchema { 41 | schema = cachedSchema 42 | } else { 43 | schema = SubSchema() 44 | cachedSchemasForTypes[_schemaName] = schema 45 | } 46 | return schema 47 | } 48 | 49 | internal static func set(key: String, forPath keyPath: AnyKeyPath) { 50 | if keyStringsForSchemaKeyPaths[_schemaName] == nil { 51 | keyStringsForSchemaKeyPaths[_schemaName] = [:] 52 | } 53 | keyStringsForSchemaKeyPaths[_schemaName]?[keyPath] = key 54 | } 55 | 56 | internal static func key(forPath keyPath: AnyKeyPath) -> String? { 57 | print(keyStringsForSchemaKeyPaths) 58 | return keyStringsForSchemaKeyPaths[_schemaName]?[keyPath] 59 | } 60 | } 61 | // swiftlint:enable missing_docs 62 | 63 | /** 64 | A protocol that identifies a type as representing a GraphQL 'interface'. 65 | 66 | 'Interfaces' in GraphQL are like Swift protocols that GraphQL types can declare that they implement. In Artemis, GraphQL 67 | types declare their conformance to interfaces via their static `implements` property. 68 | */ 69 | public protocol Interface: Object { } 70 | 71 | /** 72 | A protocol that designates a type as representing a GraphQL 'enum'. 73 | */ 74 | public protocol Enum: Scalar, CaseIterable, RawRepresentable, Encodable where Self.RawValue == String { } 75 | // swiftlint:disable missing_docs 76 | extension Enum where Result == Self { 77 | public static var `default`: Self { 78 | return self.allCases[self.allCases.startIndex] 79 | } 80 | 81 | public func encode(to encoder: Encoder) throws { 82 | var container = encoder.singleValueContainer() 83 | try container.encode(_EncodedEnum(rawValue: self.rawValue)) 84 | } 85 | 86 | public static func createUnsafeResult(from object: Any, key: String) throws -> Result { 87 | guard let raw = object as? String, 88 | let returnValue = Self(rawValue: raw) 89 | else { throw GraphQLError.singleItemParseFailure(operation: key) } 90 | return returnValue 91 | } 92 | } 93 | // swiftlint:enable missing_docs 94 | 95 | internal struct _EncodedEnum: _SelectionInput, Encodable { 96 | internal var rawValue: String 97 | internal func render() -> String { 98 | return rawValue 99 | } 100 | } 101 | 102 | /** 103 | A protocol that designates a type as representing a GraphQL 'input object' type. 104 | */ 105 | public protocol Input: _SelectionInput, Encodable { } 106 | 107 | // MARK: - 108 | 109 | /// An internal superprotocol for `Object` that allows referencing the schema name as a string. 110 | public protocol _AnyObject { 111 | /// A string name for the `SubSchema` of this `Object`. 112 | static var _schemaName: String { get } 113 | } 114 | 115 | /** 116 | A type used for objects to declare the interfaces that they implement. 117 | */ 118 | public struct Interfaces: AnyInterfaces { } 119 | 120 | /** 121 | A type used to declare that an object conforms to no interfaces. 122 | */ 123 | public protocol AnyInterfaces { 124 | // swiftlint:disable missing_docs 125 | associatedtype I1; associatedtype I2; associatedtype I3; associatedtype I4; associatedtype I5 126 | } 127 | 128 | // MARK: - 129 | extension Interfaces where I5: Interface, I4: Interface, I3: Interface, I2: Interface, I1: Interface { 130 | public init(_ i1: I1.Type, _ i2: I2.Type, _ i3: I3.Type, _ i4: I4.Type, _ i5: I5.Type) { } 131 | } 132 | extension Interfaces where I5 == Void, I4: Interface, I3: Interface, I2: Interface, I1: Interface { 133 | public init(_ i1: I1.Type, _ i2: I2.Type, _ i3: I3.Type, _ i4: I4.Type) { } 134 | } 135 | extension Interfaces where I5 == Void, I4 == Void, I3: Interface, I2: Interface, I1: Interface { 136 | public init(_ i1: I1.Type, _ i2: I2.Type, _ i3: I3.Type) { } 137 | } 138 | extension Interfaces where I5 == Void, I4 == Void, I3 == Void, I2: Interface, I1: Interface { 139 | public init(_ i1: I1.Type, _ i2: I2.Type) { } 140 | } 141 | extension Interfaces where I5 == Void, I4 == Void, I3 == Void, I2 == Void, I1: Interface { 142 | public init(_ i1: I1.Type) { } 143 | } 144 | extension Interfaces where I5 == Void, I4 == Void, I3 == Void, I2 == Void, I1 == Void { 145 | public init() { } 146 | } 147 | 148 | extension Enum { 149 | func render() -> String { 150 | return self.rawValue 151 | } 152 | } 153 | 154 | extension Input { 155 | func render() -> String { 156 | return "" 157 | } 158 | } 159 | 160 | extension Object where ImplementedInterfaces == Interfaces { 161 | public static var implements: ImplementedInterfaces { return Interfaces() } 162 | } 163 | // swiftlint:enable missing_docs 164 | 165 | extension Object { 166 | static func createUnsafeResult(from object: Any, key: String) throws -> Result { 167 | guard let dictRepresentation = object as? [String: Any], 168 | let result = Partial(values: dictRepresentation) as? Result 169 | else { throw GraphQLError.singleItemParseFailure(operation: key) } 170 | return result 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/_Operation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An object that represents an operation (either a query or mutation) performed with a GraphQL API. 5 | 6 | Operation objects are used with `Client` objects to make requests with a GraphQL API. In this relationship, operations 7 | describe the GraphQL request document by being created with the selected fields. Operations are instantiated using the 8 | static `query` and `mutation` functions. 9 | 10 | These objects are generally created inside a `Client.perform(_:completion:)` method call so that their `Schema` and 11 | `Result` types can be inferred from the selections done inside their function builders. 12 | */ 13 | public struct _Operation { 14 | enum OperationType { 15 | case query 16 | case mutation 17 | } 18 | 19 | private let name: String? 20 | let error: GraphQLError? 21 | let resultCreator: ([String: Any]) throws -> Result 22 | let renderedSelectionSets: String 23 | let renderedFragments: String 24 | let operationType: OperationType 25 | 26 | fileprivate init( 27 | _ type: OperationType, 28 | on: Q.Type, 29 | name: String? = nil, 30 | @_SelectionSetBuilder _ selection: (_Selector) -> _SelectionSet 31 | ) { 32 | self.operationType = type 33 | self.name = name 34 | let fieldsAggegate = selection(_Selector()) 35 | self.error = fieldsAggegate.error 36 | self.renderedSelectionSets = fieldsAggegate.render() 37 | self.resultCreator = { try fieldsAggegate.createResult(from: $0) } 38 | self.renderedFragments = Set(fieldsAggegate.renderedFragmentDeclarations).sorted().joined(separator: ",") 39 | } 40 | 41 | func render() -> String { 42 | var nameString = "" 43 | if let name = self.name { 44 | nameString = " \(name)" 45 | } 46 | let opName: String 47 | if self.operationType == .query && nameString.isEmpty { 48 | opName = "" 49 | } else if self.operationType == .query { 50 | opName = "query\(nameString)" 51 | } else { 52 | opName = "mutation\(nameString)" 53 | } 54 | let fragmentString = (self.renderedFragments.isEmpty) ? "" : ",\(self.renderedFragments)" 55 | return "\(opName){\(self.renderedSelectionSets)}\(fragmentString)" 56 | } 57 | 58 | func renderDebug() -> String { 59 | let ugly = self.render() 60 | 61 | var result: String = "" 62 | 63 | var tabs = "" 64 | var isOnArgumentsLine = false 65 | var previousChar: Character? 66 | for char in ugly { 67 | if char == "(" { 68 | isOnArgumentsLine = true 69 | } 70 | 71 | if char == "{" { 72 | if previousChar != nil && previousChar != "\n" && previousChar != " " { 73 | result.append(" ") 74 | } else { 75 | result.append(tabs) 76 | } 77 | isOnArgumentsLine = false 78 | result.append(char) 79 | result.append("\n") 80 | tabs.append(" ") 81 | result.append(tabs) 82 | } else if char == "," { 83 | if isOnArgumentsLine { 84 | result.append(", ") 85 | } else { 86 | result.append("\n") 87 | result.append(tabs) 88 | } 89 | } else if char == "}" { 90 | tabs.removeLast(3) 91 | result.append("\n") 92 | result.append(tabs) 93 | result.append(char) 94 | } else if char == ":" { 95 | result.append(": ") 96 | } else if char == "\\" { 97 | continue 98 | } else { 99 | result.append(char) 100 | } 101 | previousChar = char 102 | } 103 | 104 | return result 105 | } 106 | 107 | func createResult(from data: Data) throws -> Result { 108 | guard let jsonResult = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? [String: Any] else { 109 | if let dataString = String(data: data, encoding: .utf8) { 110 | throw GraphQLError.malformattedResponse(reason: "Data wasn't a JSON object: '\(dataString)'") 111 | } else { 112 | throw GraphQLError.malformattedResponse(reason: "Data wasn't a JSON object") 113 | } 114 | } 115 | if let errors = jsonResult["errors"] as? [[String: Any]] { 116 | let errorMessages = errors.map { $0["message"] as? String }.compactMap { $0 } 117 | throw GraphQLError.invalidRequest(reasons: errorMessages) 118 | } 119 | guard let dataDict = jsonResult["data"] as? [String: Any] else { 120 | throw GraphQLError.malformattedResponse(reason: "JSON result didn't include a 'data' object") 121 | } 122 | return try resultCreator(dataDict) 123 | } 124 | } 125 | 126 | extension _Operation { 127 | /** 128 | Creates a query operation with the given field selection. 129 | - parameter name: An optional debug name for the operation sent along with the document. 130 | - parameter selection: The selected set of fields to include with the document. 131 | */ 132 | public static func query( 133 | name: String? = nil, 134 | @_SelectionSetBuilder _ selection: (_Selector) -> _SelectionSet 135 | ) -> _Operation { 136 | return _Operation(.query, on: S.QueryType.self, name: name, selection) 137 | } 138 | 139 | /** 140 | Creates a mutation operation with the given field selection. 141 | - parameter name: An optional debug name for the operation sent along with the document. 142 | - parameter selection: The selected set of fields to include with the document. 143 | */ 144 | public static func mutation( 145 | name: String? = nil, 146 | @_SelectionSetBuilder _ selection: (_Selector) -> _SelectionSet 147 | ) -> _Operation where S.MutationType: Object { 148 | return _Operation(.mutation, on: S.MutationType.self, name: name, selection) 149 | } 150 | 151 | /** 152 | Creates a query operation with the given field selection. 153 | - parameter name: An optional debug name for the operation sent along with the document. 154 | - parameter selection: The selected set of fields to include with the document. 155 | */ 156 | public static func query( 157 | name: String? = nil, 158 | @_SelectionSetBuilder _ selection: () -> _SelectionSet 159 | ) -> _Operation { 160 | return _Operation(.query, on: S.QueryType.self, name: name, { _ in return selection() }) 161 | } 162 | 163 | /** 164 | Creates a mutation operation with the given field selection. 165 | - parameter name: An optional debug name for the operation sent along with the document. 166 | - parameter selection: The selected set of fields to include with the document. 167 | */ 168 | public static func mutation( 169 | name: String? = nil, 170 | @_SelectionSetBuilder _ selection: () -> _SelectionSet 171 | ) -> _Operation where S.MutationType: Object { 172 | return _Operation(.mutation, on: S.MutationType.self, name: name, { _ in return selection() }) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Artemis/3. Handling/Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An object that can make network requests for a `Client` object. 5 | 6 | Custom objects conforming to this protocol can be used to send GraphQL documents to an API in a custom way (for example, 7 | if you want to send them via a custom application layer protocol or sockets directly over TCP). 8 | */ 9 | public protocol NetworkDelegate { 10 | /** 11 | Has the network delegate send the given GraphQL document string to the server, calling the given completion handler 12 | with the resulting raw data. 13 | 14 | - parameter document: The parsed GraphQL document to send. 15 | - parameter completion: A completion handler that is called with either the resulting request data or an error. 16 | */ 17 | func send(document: String, completion: @escaping (Result) -> Void) 18 | } 19 | 20 | /** 21 | An object that can interact with a GraphQL API. 22 | 23 | Client objects are the base object used to make requests to a GraphQL API. You should generally keep one instance of 24 | this object that all interaction with your GraphQL API via a singleton, SwiftUI environment object, or through other 25 | dependency management. 26 | 27 | Client objects will parse `Operation` objects into document strings, then use a 'network delegate' to attach the 28 | rendered document and perform the actual networking. The network delegate will give back the raw `Data` from the 29 | response, which the client object will then turn into the appropriate result type (likely a `Partial` wrapper of the 30 | expected query selection type). 31 | */ 32 | open class Client { 33 | /// The object that performs the actual network requests for this client. 34 | public let networkDelegate: NetworkDelegate // swiftlint:disable:this weak_delegate 35 | /// Whether the client logs requests and responses. 36 | public var loggingEnabled = true 37 | 38 | /** 39 | Creates a new client object that sends operations to the given URL. 40 | 41 | When using this initializer, the client object will use a default `HTTPNetworkingDelegate` as its network delegate. 42 | Customization of sent requests can optionally be done by providing the HTTP method (either GET or POST) that 43 | requests will use, as well as an optional 'request builder' closure that all new request objects will be given to 44 | after being setup. You can use this method to do things like attach authorization headers. 45 | 46 | - parameter endpoint: The full URL for your GraphQL network API. Required. 47 | - parameter method: The HTTP method (GET or POST) that the client should use to send requests to the given 48 | endpoint. If not provided, the default is `.post`, meaning it will attach GraphQL documents to the body of the 49 | request. 50 | - parameter requestBuilder: A closure the client will call with all newly-created request objects to allow headers 51 | or other values to be customized. The request object will already be populated with the GraphQL document, 52 | default headers, and the endpoint. 53 | */ 54 | public init( 55 | endpoint: URL, 56 | method: HTTPNetworkingDelegate.Method = .post, 57 | requestBuilder: ((_ request: URLRequest) -> Void)? = nil 58 | ) { 59 | self.networkDelegate = HTTPNetworkingDelegate(endpoint: endpoint, method: method) 60 | } 61 | 62 | /** 63 | Creates a new client object that sends requests through the given network delegate. 64 | 65 | When using this initializer, all requests for the client to perform API requests will be routed to the given 66 | network delegate. This method can be used if networking to your GraphQL API is not done over HTTP (for example, if 67 | you're using sending the document string directly over a transport layer protocol). 68 | 69 | - parameter networkDelegate: An object conforming to `NetworkDelegate` that will accept GraphQL document strings 70 | and send them to the server on behalf of the client object. 71 | */ 72 | public init(networkDelegate: NetworkDelegate) { 73 | self.networkDelegate = networkDelegate 74 | } 75 | 76 | /** 77 | Performs the given GraphQL operation. 78 | 79 | When this method is called, the client object will render the given GraphQL operation into a document string then 80 | have its 'network delegate' object send the document to its backend service to perform. When the service response 81 | returns, the object is parsed and returned as an appropriate `Result` type. 82 | 83 | - parameter operation: The GraphQL operation to perform. 84 | - parameter mock: A mocked response that the client should use instead of making a real network request. 85 | - parameter completion: The completion handler to call with the result of the GraphQL operation. 86 | */ 87 | open func perform( 88 | _ operation: _Operation, 89 | mock: Data? = nil, 90 | completion: @escaping (Result) -> Void 91 | ) { 92 | if let error = operation.error { 93 | assertionFailure("Built query was not valid - \(error)") 94 | return 95 | } 96 | if self.loggingEnabled { print("Artemis - fetching query:\n\(operation.renderDebug())") } 97 | if let data = mock { 98 | do { 99 | if self.loggingEnabled, 100 | let object = try? JSONSerialization.jsonObject(with: data, options: []), 101 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 102 | let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { 103 | print("Artemis - mock response:\n\(prettyPrintedString)") 104 | } 105 | let result = try operation.createResult(from: data) 106 | completion(.success(result)) 107 | } catch { 108 | completion(.failure(error as? GraphQLError ?? GraphQLError.other(error))) 109 | } 110 | } else { 111 | self.networkDelegate.send(document: operation.render()) { [weak self] rawResult in 112 | do { 113 | let data = try rawResult.get() 114 | if self?.loggingEnabled == true, 115 | let object = try? JSONSerialization.jsonObject(with: data, options: []), 116 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 117 | let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { 118 | print("Artemis - received response:\n\(prettyPrintedString)") 119 | } 120 | let result = try operation.createResult(from: data) 121 | completion(.success(result)) 122 | } catch { 123 | completion(.failure(error as? GraphQLError ?? GraphQLError.other(error))) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | extension Data { 131 | fileprivate var prettyPrintedJSONString: NSString? { 132 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []), 133 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 134 | let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return nil } 135 | 136 | return prettyPrintedString 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TypeTestCases.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | protocol TypeTestCase { 5 | // The type the render test is testing that we can select (e.g. Int, Float, TestObject) 6 | associatedtype SelectionType 7 | // The type the render test is selecting on (e.g. Interface1, TestObject) 8 | associatedtype SelectionBase 9 | } 10 | 11 | // MARK: - On TestObject 12 | 13 | final class TestObject_String_TypeTests: XCTestCase, TypeTestCase { 14 | typealias SelectionBase = TestObject 15 | typealias SelectionType = String 16 | } 17 | 18 | final class TestObject_Int_TypeTests: XCTestCase, TypeTestCase { 19 | typealias SelectionBase = TestObject 20 | typealias SelectionType = Int 21 | } 22 | 23 | final class TestObject_Bool_TypeTests: XCTestCase, TypeTestCase { 24 | typealias SelectionBase = TestObject 25 | typealias SelectionType = Bool 26 | } 27 | 28 | final class TestObject_Float_TypeTests: XCTestCase, TypeTestCase { 29 | typealias SelectionBase = TestObject 30 | typealias SelectionType = Float 31 | } 32 | 33 | final class TestObject_Double_TypeTests: XCTestCase, TypeTestCase { 34 | typealias SelectionBase = TestObject 35 | typealias SelectionType = Double 36 | } 37 | 38 | final class TestObject_TestObject_TypeTests: XCTestCase, TypeTestCase { 39 | typealias SelectionBase = TestObject 40 | typealias SelectionType = TestObject 41 | } 42 | 43 | final class TestObject_TestEnum_TypeTests: XCTestCase, TypeTestCase { 44 | typealias SelectionBase = TestObject 45 | typealias SelectionType = TestEnum 46 | } 47 | 48 | // MARK: - On TestInterface1 49 | 50 | final class TestInterface1_String_TypeTests: XCTestCase, TypeTestCase { 51 | typealias SelectionBase = TestInterface1 52 | typealias SelectionType = String 53 | } 54 | 55 | final class TestInterface1_Int_TypeTests: XCTestCase, TypeTestCase { 56 | typealias SelectionBase = TestInterface1 57 | typealias SelectionType = Int 58 | } 59 | 60 | final class TestInterface1_Bool_TypeTests: XCTestCase, TypeTestCase { 61 | typealias SelectionBase = TestInterface1 62 | typealias SelectionType = Bool 63 | } 64 | 65 | final class TestInterface1_Float_TypeTests: XCTestCase, TypeTestCase { 66 | typealias SelectionBase = TestInterface1 67 | typealias SelectionType = Float 68 | } 69 | 70 | final class TestInterface1_Double_TypeTests: XCTestCase, TypeTestCase { 71 | typealias SelectionBase = TestInterface1 72 | typealias SelectionType = Double 73 | } 74 | 75 | final class TestInterface1_TestObject_TypeTests: XCTestCase, TypeTestCase { 76 | typealias SelectionBase = TestInterface1 77 | typealias SelectionType = TestObject 78 | } 79 | 80 | final class TestInterface1_TestEnum_TypeTests: XCTestCase, TypeTestCase { 81 | typealias SelectionBase = TestInterface1 82 | typealias SelectionType = TestEnum 83 | } 84 | 85 | // MARK: - On TestInterface2 86 | 87 | final class TestInterface2_String_TypeTests: XCTestCase, TypeTestCase { 88 | typealias SelectionBase = TestInterface2 89 | typealias SelectionType = String 90 | } 91 | 92 | final class TestInterface2_Int_TypeTests: XCTestCase, TypeTestCase { 93 | typealias SelectionBase = TestInterface2 94 | typealias SelectionType = Int 95 | } 96 | 97 | final class TestInterface2_Bool_TypeTests: XCTestCase, TypeTestCase { 98 | typealias SelectionBase = TestInterface2 99 | typealias SelectionType = Bool 100 | } 101 | 102 | final class TestInterface2_Float_TypeTests: XCTestCase, TypeTestCase { 103 | typealias SelectionBase = TestInterface2 104 | typealias SelectionType = Float 105 | } 106 | 107 | final class TestInterface2_Double_TypeTests: XCTestCase, TypeTestCase { 108 | typealias SelectionBase = TestInterface2 109 | typealias SelectionType = Double 110 | } 111 | 112 | final class TestInterface2_TestObject_TypeTests: XCTestCase, TypeTestCase { 113 | typealias SelectionBase = TestInterface2 114 | typealias SelectionType = TestObject 115 | } 116 | 117 | final class TestInterface2_TestEnum_TypeTests: XCTestCase, TypeTestCase { 118 | typealias SelectionBase = TestInterface2 119 | typealias SelectionType = TestEnum 120 | } 121 | 122 | // MARK: - On TestInterface3 123 | 124 | final class TestInterface3_String_TypeTests: XCTestCase, TypeTestCase { 125 | typealias SelectionBase = TestInterface3 126 | typealias SelectionType = String 127 | } 128 | 129 | final class TestInterface3_Int_TypeTests: XCTestCase, TypeTestCase { 130 | typealias SelectionBase = TestInterface3 131 | typealias SelectionType = Int 132 | } 133 | 134 | final class TestInterface3_Bool_TypeTests: XCTestCase, TypeTestCase { 135 | typealias SelectionBase = TestInterface3 136 | typealias SelectionType = Bool 137 | } 138 | 139 | final class TestInterface3_Float_TypeTests: XCTestCase, TypeTestCase { 140 | typealias SelectionBase = TestInterface3 141 | typealias SelectionType = Float 142 | } 143 | 144 | final class TestInterface3_Double_TypeTests: XCTestCase, TypeTestCase { 145 | typealias SelectionBase = TestInterface3 146 | typealias SelectionType = Double 147 | } 148 | 149 | final class TestInterface3_TestObject_TypeTests: XCTestCase, TypeTestCase { 150 | typealias SelectionBase = TestInterface3 151 | typealias SelectionType = TestObject 152 | } 153 | 154 | final class TestInterface3_TestEnum_TypeTests: XCTestCase, TypeTestCase { 155 | typealias SelectionBase = TestInterface3 156 | typealias SelectionType = TestEnum 157 | } 158 | 159 | // MARK: - On TestInterface4 160 | 161 | final class TestInterface4_String_TypeTests: XCTestCase, TypeTestCase { 162 | typealias SelectionBase = TestInterface4 163 | typealias SelectionType = String 164 | } 165 | 166 | final class TestInterface4_Int_TypeTests: XCTestCase, TypeTestCase { 167 | typealias SelectionBase = TestInterface4 168 | typealias SelectionType = Int 169 | } 170 | 171 | final class TestInterface4_Bool_TypeTests: XCTestCase, TypeTestCase { 172 | typealias SelectionBase = TestInterface4 173 | typealias SelectionType = Bool 174 | } 175 | 176 | final class TestInterface4_Float_TypeTests: XCTestCase, TypeTestCase { 177 | typealias SelectionBase = TestInterface4 178 | typealias SelectionType = Float 179 | } 180 | 181 | final class TestInterface4_Double_TypeTests: XCTestCase, TypeTestCase { 182 | typealias SelectionBase = TestInterface4 183 | typealias SelectionType = Double 184 | } 185 | 186 | final class TestInterface4_TestObject_TypeTests: XCTestCase, TypeTestCase { 187 | typealias SelectionBase = TestInterface4 188 | typealias SelectionType = TestObject 189 | } 190 | 191 | final class TestInterface4_TestEnum_TypeTests: XCTestCase, TypeTestCase { 192 | typealias SelectionBase = TestInterface4 193 | typealias SelectionType = TestEnum 194 | } 195 | 196 | // MARK: - On TestInterface5 197 | 198 | final class TestInterface5_String_TypeTests: XCTestCase, TypeTestCase { 199 | typealias SelectionBase = TestInterface5 200 | typealias SelectionType = String 201 | } 202 | 203 | final class TestInterface5_Int_TypeTests: XCTestCase, TypeTestCase { 204 | typealias SelectionBase = TestInterface5 205 | typealias SelectionType = Int 206 | } 207 | 208 | final class TestInterface5_Bool_TypeTests: XCTestCase, TypeTestCase { 209 | typealias SelectionBase = TestInterface5 210 | typealias SelectionType = Bool 211 | } 212 | 213 | final class TestInterface5_Float_TypeTests: XCTestCase, TypeTestCase { 214 | typealias SelectionBase = TestInterface5 215 | typealias SelectionType = Float 216 | } 217 | 218 | final class TestInterface5_Double_TypeTests: XCTestCase, TypeTestCase { 219 | typealias SelectionBase = TestInterface5 220 | typealias SelectionType = Double 221 | } 222 | 223 | final class TestInterface5_TestObject_TypeTests: XCTestCase, TypeTestCase { 224 | typealias SelectionBase = TestInterface5 225 | typealias SelectionType = TestObject 226 | } 227 | 228 | final class TestInterface5_TestEnum_TypeTests: XCTestCase, TypeTestCase { 229 | typealias SelectionBase = TestInterface5 230 | typealias SelectionType = TestEnum 231 | } 232 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/Other/ConditionalTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Artemis 3 | 4 | class ConditionalTests: XCTestCase { 5 | func testIfTrue() { 6 | let include = true 7 | let query: _Operation> = .query { 8 | $0.testObject { 9 | $0.int 10 | if include { 11 | $0.bool 12 | } 13 | } 14 | } 15 | let response = Data(""" 16 | { 17 | "data": { 18 | "testObject": { 19 | "int": 123, 20 | "bool": true 21 | } 22 | } 23 | } 24 | """.utf8) 25 | 26 | XCTAssertEqual(query.render(), "{testObject{int,bool}}") 27 | let res = try? query.createResult(from: response) 28 | XCTAssertEqual(res?.int, 123) 29 | XCTAssertEqual(res?.bool, true) 30 | } 31 | 32 | func testIfFalse() { 33 | let include = false 34 | let query: _Operation> = .query { 35 | $0.testObject { 36 | $0.int 37 | if include { 38 | $0.bool 39 | } 40 | } 41 | } 42 | let response = Data(""" 43 | { 44 | "data": { 45 | "testObject": { 46 | "int": 123 47 | } 48 | } 49 | } 50 | """.utf8) 51 | 52 | XCTAssertEqual(query.render(), "{testObject{int}}") 53 | let res = try? query.createResult(from: response) 54 | XCTAssertEqual(res?.int, 123) 55 | XCTAssertEqual(res?.bool, nil) 56 | } 57 | 58 | func testIfElseTrue() { 59 | let include = true 60 | let query: _Operation> = .query { 61 | $0.testObject { 62 | if include { 63 | $0.bool 64 | } else { 65 | $0.string 66 | } 67 | } 68 | } 69 | let response = Data(""" 70 | { 71 | "data": { 72 | "testObject": { 73 | "bool": true 74 | } 75 | } 76 | } 77 | """.utf8) 78 | 79 | XCTAssertEqual(query.render(), "{testObject{bool}}") 80 | let res = try? query.createResult(from: response) 81 | XCTAssertEqual(res?.bool, true) 82 | XCTAssertEqual(res?.string, nil) 83 | } 84 | 85 | func testIfElseFalse() { 86 | let include = false 87 | let query: _Operation> = .query { 88 | $0.testObject { 89 | if include { 90 | $0.bool 91 | } else { 92 | $0.string 93 | } 94 | } 95 | } 96 | let response = Data(""" 97 | { 98 | "data": { 99 | "testObject": { 100 | "string": "value" 101 | } 102 | } 103 | } 104 | """.utf8) 105 | 106 | XCTAssertEqual(query.render(), "{testObject{string}}") 107 | let res = try? query.createResult(from: response) 108 | XCTAssertEqual(res?.bool, nil) 109 | XCTAssertEqual(res?.string, "value") 110 | } 111 | } 112 | 113 | extension ConditionalTests { 114 | func testIfElseTrueWithOther() { 115 | let include = true 116 | let query: _Operation> = .query { 117 | $0.testObject { 118 | $0.int 119 | if include { 120 | $0.bool 121 | } else { 122 | $0.string 123 | } 124 | } 125 | } 126 | let response = Data(""" 127 | { 128 | "data": { 129 | "testObject": { 130 | "int": 123, 131 | "bool": true 132 | } 133 | } 134 | } 135 | """.utf8) 136 | 137 | XCTAssertEqual(query.render(), "{testObject{int,bool}}") 138 | let res = try? query.createResult(from: response) 139 | XCTAssertEqual(res?.int, 123) 140 | XCTAssertEqual(res?.bool, true) 141 | XCTAssertEqual(res?.string, nil) 142 | } 143 | 144 | func testIfElseFalseWithOther() { 145 | let include = false 146 | let query: _Operation> = .query { 147 | $0.testObject { 148 | $0.int 149 | if include { 150 | $0.bool 151 | } else { 152 | $0.string 153 | } 154 | } 155 | } 156 | let response = Data(""" 157 | { 158 | "data": { 159 | "testObject": { 160 | "int": 123, 161 | "string": "value" 162 | } 163 | } 164 | } 165 | """.utf8) 166 | 167 | XCTAssertEqual(query.render(), "{testObject{int,string}}") 168 | let res = try? query.createResult(from: response) 169 | XCTAssertEqual(res?.int, 123) 170 | XCTAssertEqual(res?.bool, nil) 171 | XCTAssertEqual(res?.string, "value") 172 | } 173 | } 174 | 175 | extension ConditionalTests { 176 | func testIfTrueMultiple() { 177 | let include = true 178 | let query: _Operation> = .query { 179 | $0.testObject { 180 | $0.int 181 | if include { 182 | $0.bool 183 | $0.double 184 | } 185 | } 186 | } 187 | let response = Data(""" 188 | { 189 | "data": { 190 | "testObject": { 191 | "int": 123, 192 | "bool": true, 193 | "double": 1.23 194 | } 195 | } 196 | } 197 | """.utf8) 198 | 199 | XCTAssertEqual(query.render(), "{testObject{int,bool,double}}") 200 | let res = try? query.createResult(from: response) 201 | XCTAssertEqual(res?.int, 123) 202 | XCTAssertEqual(res?.bool, true) 203 | XCTAssertEqual(res?.double, 1.23) 204 | } 205 | 206 | func testIfFalseMultiple() { 207 | let include = false 208 | let query: _Operation> = .query { 209 | $0.testObject { 210 | $0.int 211 | if include { 212 | $0.bool 213 | $0.double 214 | } 215 | } 216 | } 217 | let response = Data(""" 218 | { 219 | "data": { 220 | "testObject": { 221 | "int": 123 222 | } 223 | } 224 | } 225 | """.utf8) 226 | 227 | XCTAssertEqual(query.render(), "{testObject{int}}") 228 | let res = try? query.createResult(from: response) 229 | XCTAssertEqual(res?.int, 123) 230 | XCTAssertEqual(res?.bool, nil) 231 | XCTAssertEqual(res?.double, nil) 232 | } 233 | 234 | func testIfElseTrueMultiple() { 235 | let include = true 236 | let query: _Operation> = .query { 237 | $0.testObject { 238 | if include { 239 | $0.bool 240 | $0.double 241 | } else { 242 | $0.string 243 | $0.int 244 | } 245 | } 246 | } 247 | let response = Data(""" 248 | { 249 | "data": { 250 | "testObject": { 251 | "bool": true, 252 | "double": 1.23 253 | } 254 | } 255 | } 256 | """.utf8) 257 | 258 | XCTAssertEqual(query.render(), "{testObject{bool,double}}") 259 | let res = try? query.createResult(from: response) 260 | XCTAssertEqual(res?.bool, true) 261 | XCTAssertEqual(res?.double, 1.23) 262 | XCTAssertEqual(res?.string, nil) 263 | XCTAssertEqual(res?.int, nil) 264 | } 265 | 266 | func testIfElseFalseMultiple() { 267 | let include = false 268 | let query: _Operation> = .query { 269 | $0.testObject { 270 | if include { 271 | $0.bool 272 | $0.double 273 | } else { 274 | $0.string 275 | $0.int 276 | } 277 | } 278 | } 279 | let response = Data(""" 280 | { 281 | "data": { 282 | "testObject": { 283 | "string": "value", 284 | "int": 123 285 | } 286 | } 287 | } 288 | """.utf8) 289 | 290 | XCTAssertEqual(query.render(), "{testObject{string,int}}") 291 | let res = try? query.createResult(from: response) 292 | XCTAssertEqual(res?.bool, nil) 293 | XCTAssertEqual(res?.double, nil) 294 | XCTAssertEqual(res?.string, "value") 295 | XCTAssertEqual(res?.int, 123) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Sources/Artemis/2. Selecting/_ArgumentEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class _ArgumentEncoder { 4 | func encode(_ value: T) throws -> String { 5 | let encoder = ArgumentEncoding(data: .init(), codingPath: []) 6 | try value.encode(to: encoder) 7 | guard let (_, parsedValue) = encoder.data.values.first(where: { key, _ in key == "root" }) else { return "" } 8 | return parsedValue.rendered(isRoot: true) 9 | } 10 | 11 | func encode(_ value: T) throws -> [Argument] { 12 | let encoder = ArgumentEncoding(data: .init(), codingPath: []) 13 | try value.encode(to: encoder) 14 | guard let (_, parsedValue) = encoder.data.values.first(where: { key, _ in key == "root" }) else { return [] } 15 | switch parsedValue { 16 | case .value, .array: return [] 17 | case .object(let object): 18 | let args: [Argument] = object 19 | .map { $0 as? (String, DataType) } 20 | .compactMap { $0 } 21 | .map { Argument(name: $0.0, value: $0.1.rendered(isRoot: false)) } 22 | return args 23 | } 24 | } 25 | } 26 | 27 | private enum DataType { 28 | case value(_SelectionInput) 29 | case array(NSMutableArray) 30 | case object(NSMutableArray) 31 | 32 | func rendered(isRoot: Bool) -> String { 33 | switch self { 34 | case .value(let value): 35 | return isRoot ? "(\(value.render())" : value.render() 36 | case .array(let array): 37 | let values = array 38 | .map { $0 as? DataType } 39 | .compactMap { $0 } 40 | .map { $0.rendered(isRoot: false) } 41 | .joined(separator: ",") 42 | return isRoot ? "([\(values)])" : "[\(values)]" 43 | case .object(let object): 44 | let values = object 45 | .map { $0 as? (String, DataType) } 46 | .compactMap { $0 } 47 | .map { "\($0):\($1.rendered(isRoot: false))" } 48 | .joined(separator: ",") 49 | return isRoot ? "(\(values))" : "{\(values)}" 50 | } 51 | } 52 | } 53 | 54 | private struct ArgumentEncoding: Encoder { 55 | final class Data { 56 | private(set) var values: [(String, DataType)] = [] 57 | 58 | func add(_ dataType: DataType, codingPath: [CodingKey]) { 59 | let key = codingPath.last?.stringValue ?? "root" 60 | self.values.append((key, dataType)) 61 | } 62 | } 63 | 64 | var data: Data 65 | var codingPath: [CodingKey] 66 | var userInfo: [CodingUserInfoKey: Any] = [:] 67 | 68 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { 69 | let dict = NSMutableArray() 70 | data.add(.object(dict), codingPath: codingPath) 71 | let container = ArgumentKeyedEncoding(data: data, dict: dict, codingPath: codingPath) 72 | return KeyedEncodingContainer(container) 73 | } 74 | 75 | func unkeyedContainer() -> UnkeyedEncodingContainer { 76 | let array = NSMutableArray() 77 | data.add(.array(array), codingPath: codingPath) 78 | return ArgumentUnkeyedEncoding(data: data, array: array, codingPath: codingPath) 79 | } 80 | 81 | func singleValueContainer() -> SingleValueEncodingContainer { 82 | return self 83 | } 84 | } 85 | 86 | private struct ArgumentKeyedEncoding: KeyedEncodingContainerProtocol { 87 | let data: ArgumentEncoding.Data 88 | let dict: NSMutableArray 89 | var codingPath: [CodingKey] 90 | 91 | mutating func encodeNil(forKey key: Key) throws { 92 | dict.add((key.stringValue, DataType.value("nil"))) 93 | } 94 | mutating func encode(_ value: Bool, forKey key: Key) throws { 95 | dict.add((key.stringValue, DataType.value(value))) 96 | } 97 | mutating func encode(_ value: String, forKey key: Key) throws { 98 | dict.add((key.stringValue, DataType.value(value))) 99 | } 100 | mutating func encode(_ value: Double, forKey key: Key) throws { 101 | dict.add((key.stringValue, DataType.value(value))) 102 | } 103 | mutating func encode(_ value: Float, forKey key: Key) throws { 104 | dict.add((key.stringValue, DataType.value(value))) 105 | } 106 | mutating func encode(_ value: Int, forKey key: Key) throws { 107 | dict.add((key.stringValue, DataType.value(value))) 108 | } 109 | mutating func encode(_ value: Int8, forKey key: Key) throws { } 110 | mutating func encode(_ value: Int16, forKey key: Key) throws { } 111 | mutating func encode(_ value: Int32, forKey key: Key) throws { } 112 | mutating func encode(_ value: Int64, forKey key: Key) throws { } 113 | mutating func encode(_ value: UInt, forKey key: Key) throws { } 114 | mutating func encode(_ value: UInt8, forKey key: Key) throws { } 115 | mutating func encode(_ value: UInt16, forKey key: Key) throws { } 116 | mutating func encode(_ value: UInt32, forKey key: Key) throws { } 117 | mutating func encode(_ value: UInt64, forKey key: Key) throws { } 118 | 119 | mutating func encode(_ value: T, forKey key: Key) throws { 120 | let encoder = ArgumentEncoding(data: .init(), codingPath: [key]) 121 | try value.encode(to: encoder) 122 | encoder.data.values.forEach { dict.add(($0, $1)) } 123 | } 124 | 125 | mutating func nestedContainer( 126 | keyedBy keyType: NestedKey.Type, 127 | forKey key: Key 128 | ) -> KeyedEncodingContainer { 129 | let dict = NSMutableArray() 130 | self.dict.add((key.stringValue, dict)) 131 | let container = ArgumentKeyedEncoding(data: data, dict: dict, codingPath: codingPath + [key]) 132 | return KeyedEncodingContainer(container) 133 | } 134 | 135 | mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 136 | let array = NSMutableArray() 137 | self.dict.add((key.stringValue, dict)) 138 | return ArgumentUnkeyedEncoding(data: data, array: array, codingPath: codingPath + [key]) 139 | } 140 | 141 | mutating func superEncoder() -> Encoder { 142 | guard let superKey = Key(stringValue: "super") else { fatalError("Invalid key value") } 143 | return superEncoder(forKey: superKey) 144 | } 145 | 146 | mutating func superEncoder(forKey key: Key) -> Encoder { 147 | return ArgumentEncoding(data: data, codingPath: codingPath + [key]) 148 | } 149 | } 150 | 151 | private struct ArgumentUnkeyedEncoding: UnkeyedEncodingContainer { 152 | let data: ArgumentEncoding.Data 153 | var array: NSMutableArray 154 | let codingPath: [CodingKey] 155 | 156 | var count: Int { 157 | array.count 158 | } 159 | 160 | mutating func encodeNil() throws { 161 | array.add(DataType.value("nil")) 162 | } 163 | mutating func encode(_ value: Bool) throws { 164 | array.add(DataType.value(value)) 165 | } 166 | mutating func encode(_ value: String) throws { 167 | array.add(DataType.value(value)) 168 | } 169 | mutating func encode(_ value: Double) throws { 170 | array.add(DataType.value(value)) 171 | } 172 | mutating func encode(_ value: Float) throws { 173 | array.add(DataType.value(value)) 174 | } 175 | mutating func encode(_ value: Int) throws { 176 | array.add(DataType.value(value)) 177 | } 178 | mutating func encode(_ value: Int8) throws { } 179 | mutating func encode(_ value: Int16) throws { } 180 | mutating func encode(_ value: Int32) throws { } 181 | mutating func encode(_ value: Int64) throws { } 182 | mutating func encode(_ value: UInt) throws { } 183 | mutating func encode(_ value: UInt8) throws { } 184 | mutating func encode(_ value: UInt16) throws { } 185 | mutating func encode(_ value: UInt32) throws { } 186 | mutating func encode(_ value: UInt64) throws { } 187 | 188 | mutating func encode(_ value: T) throws { 189 | let encoder = ArgumentEncoding(data: .init(), codingPath: codingPath) 190 | try value.encode(to: encoder) 191 | encoder.data.values.forEach { self.array.add($1) } 192 | } 193 | 194 | mutating func nestedContainer( 195 | keyedBy keyType: NestedKey.Type 196 | ) -> KeyedEncodingContainer { 197 | let dict = NSMutableArray() 198 | array.add(dict) 199 | let container = ArgumentKeyedEncoding(data: data, dict: dict, codingPath: codingPath) 200 | return KeyedEncodingContainer(container) 201 | } 202 | 203 | mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 204 | let subarray = NSMutableArray() 205 | array.add(subarray) 206 | return ArgumentUnkeyedEncoding(data: data, array: subarray, codingPath: codingPath) 207 | } 208 | 209 | mutating func superEncoder() -> Encoder { 210 | return ArgumentEncoding(data: data, codingPath: codingPath) 211 | } 212 | } 213 | 214 | extension ArgumentEncoding: SingleValueEncodingContainer { 215 | mutating func encodeNil() throws { 216 | data.add(.value("nil"), codingPath: codingPath) 217 | } 218 | mutating func encode(_ value: Bool) throws { 219 | data.add(.value(value), codingPath: codingPath) 220 | } 221 | mutating func encode(_ value: String) throws { 222 | data.add(.value(value), codingPath: codingPath) 223 | } 224 | mutating func encode(_ value: Double) throws { 225 | data.add(.value(value), codingPath: codingPath) 226 | } 227 | mutating func encode(_ value: Float) throws { 228 | data.add(.value(value), codingPath: codingPath) 229 | } 230 | mutating func encode(_ value: Int) throws { 231 | data.add(.value(value), codingPath: codingPath) 232 | } 233 | mutating func encode(_ value: Int8) throws { } 234 | mutating func encode(_ value: Int16) throws { } 235 | mutating func encode(_ value: Int32) throws { } 236 | mutating func encode(_ value: Int64) throws { } 237 | mutating func encode(_ value: UInt) throws { } 238 | mutating func encode(_ value: UInt8) throws { } 239 | mutating func encode(_ value: UInt16) throws { } 240 | mutating func encode(_ value: UInt32) throws { } 241 | mutating func encode(_ value: UInt64) throws { } 242 | 243 | mutating func encode(_ value: T) throws { 244 | // Enums are sent through with the `EncodedEnum` wrapper instead of their String value so that "" isn't added 245 | if let encodedEnum = value as? _EncodedEnum { 246 | data.add(.value(encodedEnum), codingPath: codingPath) 247 | return 248 | } 249 | let encoder = ArgumentEncoding(data: data, codingPath: codingPath) 250 | try value.encode(to: encoder) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting started with Artemis 2 | 3 | > This guide assumes you've already added Artemis as a dependency to your project with Swift Package Manager. If you need help, please 4 | refer to the documentation [here](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). 5 | 6 | > It also assumes that you're reasonably familiar with GraphQL. If you're not, we recommend you take a quick look at their beginner 7 | documentation [here](https://graphql.org/learn/). 8 | 9 | ## Defining your schema 10 | 11 | The first step to using Artemis is to define your schema in Swift types that Artmis can use. This can be done fairly easily by referring to your 12 | GraphQL API's schema. In this tutorial, we're going to make the following 'countries of the world' GraphQL API in Artemis: 13 | 14 | ``` 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | countryCount: Int! 21 | countries: [Country!]! 22 | continents: [Continent!]! 23 | country(code: String!): Country 24 | } 25 | 26 | type Country { 27 | code: String! 28 | name: String! 29 | languages: [String!]! 30 | continent: Continent! 31 | } 32 | 33 | type Continent { 34 | code: String! 35 | name: String! 36 | countries: [Country!]! 37 | } 38 | ``` 39 | 40 | To start, we'll make the 'schema' and 'query' types in Swift. These will be two classes, where the 'schema' conforms to the `Schema` 41 | protocol, and the 'query' conforms to the `Object` protocol - something like this: 42 | 43 | ```swift 44 | final class MySchema: Schema { 45 | 46 | } 47 | 48 | final class Query: Object { 49 | 50 | } 51 | ``` 52 | The `Object` protocol declares that the conforming Swift type represents a GraphQL 'object' (i.e. an entity declared with `type` in the 53 | GraphQL schema). Its requirements are all there automatically for now, but we'll get to some more of them later. 54 | 55 | The `Schema` protocol declares that the conforming Swift type represents a GraphQL 'schema' (i.e. an entity declared with `schema` in the 56 | GraphQL schema). Its only requirement is a static `query` property, which the compiler should be warning about. We can fix this by adding 57 | a static `query` property to our `MySchema` object that refers to our `Query` class, like this: 58 | 59 | ```swift 60 | final class MySchema: Schema { 61 | static let query = Query() 62 | } 63 | ``` 64 | 65 | Next, we'll declare our main object types - `Country` and `Continent`. We'll do this just like we did with our `Query` type - a 66 | `final class` that conforms to `Object`: 67 | 68 | ```swift 69 | final class Country: Object { 70 | 71 | } 72 | 73 | final class Continent: Object { 74 | 75 | } 76 | ``` 77 | 78 | With all our main types declares, all that's left is to start filling in our fields. This is done using the `@Field` property wrapper on variables 79 | inside our `Object` types. Here's what our `Country` type looks like with the `code`, `name`, `languages`, and `continent` fields from our 80 | schema: 81 | 82 | ```swift 83 | final class Country: Object { 84 | @Field(key: "code") 85 | var code: String 86 | 87 | @Field(key: "name") 88 | var name: String 89 | 90 | @Field(key: "languages") 91 | var languages: [String] 92 | 93 | @Field(key: "continent") 94 | var continent: Continent 95 | } 96 | ``` 97 | As you can see, each field from the original GraphQL schema is represented as a variable with the same name and type (remember that `!` 98 | in a GraphQL schema means non-optional!). These variables are then wrapped with `@Field`, where the `Field` is instantiated with the 99 | string name of the field in the original GraphQL schema. While the variable name can technically be different from the schema, the `key` 100 | string value _must_ match the original GraphQL schema. 101 | 102 | We can now do the same with our `Continent` type: 103 | 104 | ```swift 105 | final class Continent: Object { 106 | @Field(key: "code") 107 | var code: String 108 | 109 | @Field(key: "name") 110 | var name: String 111 | 112 | @Field(key: "countries") 113 | var countries: [Country] 114 | } 115 | ``` 116 | 117 | Now that our two main types are defined, we can go back to our top-level `Query` type. One of its fields (`country`) uses arguments, so the 118 | field is declared slightly differently on the Swift type. Arguments must be represented by a type (preferably a struct) that conforms to the 119 | `ArgumentsList` protocol. The properties of this 'arguments list' type should each represent one of the arguments available to the field it's 120 | associated with. 121 | 122 | We'll describe the arguments for the `country` field as a new struct nested inside `Query`, something like this: 123 | 124 | ```swift 125 | final class Query: Object { 126 | struct CountryArgs: ArgumentsList { 127 | var code: String 128 | } 129 | } 130 | ``` 131 | > `ArgumentsList` only requires that the conforming type be `Encodable` (so the type's properties can be turned into the string arguments 132 | on the sent GraphQL document), so its requirements are automatic on this struct. 133 | 134 | Now we can declare our `country` field using the same `@Field` property wrapper. However, instead of declaring the type of this variable 135 | as simply `Country` (i.e. the return type of the field), we need to declare it as a tuple along with a reference to our 'arguments list' type. 136 | This would look like this, along with the other fields on `Query`: 137 | 138 | ```swift 139 | final class Query: Object { 140 | @Field(key: "countryCount") 141 | var countryCount: Int 142 | 143 | @Field(key: "countries") 144 | var countries: [Country] 145 | 146 | @Field(key: "continents") 147 | var continents: [Continent] 148 | 149 | @Field(key: "country") 150 | var country: (Country, CountryArgs.Type) 151 | 152 | struct CountryArgs: ArgumentsList { 153 | var code: String 154 | } 155 | } 156 | ``` 157 | 158 | Now we're ready to start using this schema to make some requests! 159 | 160 | ## Making requests 161 | 162 | Requests with Artemis are made with instances of `Client`. Most simply, this object is created with a reference to your `Schema` type then 163 | given a `URL` to your GraphQL API's endpoint - something like this: 164 | 165 | ```swift 166 | let client = Client(endpoint: URL(string: "https://myapi.com")!) 167 | ``` 168 | 169 | `Client` has a `perform` method that we then call with our field selection and a completion handler for the result. Selection starts by 170 | calling either the `.query` or `.mutation` methods (though, our API only supports queries for now, so we'll stick to just `.query`). This 171 | method is then passed in a closure that include our field selection. Without getting into the weeds of it, fields are 'selected' using a special 172 | [result builder](https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html#ID630) closure. This closure is passed in a 173 | special 'selector' object that can be used with all the same properties as our `Query` type. 174 | 175 | This is all best learned with an example. Say we want to make a request that gets all countries (via the `countries` field). Ignoring the 176 | completion handler for now, this is done by calling the `Client.perform` method like this: 177 | 178 | ```swift 179 | client.perform(.query { 180 | $0.countries 181 | }, completion: { _ in 182 | 183 | }) 184 | ``` 185 | 186 | Try and run that, and... it won't compile. This is because this is an invalid GraphQL query - we need to keep selecting properties until we 187 | reach only 'scalar' types (i.e. basic types like integers or strings), which is enforced by Artemis' type-checking. So, we'll update our 188 | `perform` call to include each country's `name`, like this: 189 | 190 | ```swift 191 | client.perform(.query { 192 | $0.countries { 193 | $0.name 194 | } 195 | }, completion: { _ in 196 | 197 | }) 198 | ``` 199 | 200 | ### Handling responses 201 | 202 | This will compile, since `name` is a `String` (a scalar value). Another noteworthy thing here is that this will also not compile if we tried to 203 | switch `countries` and `name` - Artemis checks the types of nested properties, so it knows that `name` is a property of `Country` (and 204 | that `Country` is the underlying type for the `countries` variable). In a nutshell, this means we're getting Swift compile-time checking on 205 | our GraphQL queries. 206 | 207 | This compile-time checking extends past just our requests, though - let's take a look at our completion handler. The value passed into this 208 | handler is a native Swift `Result` enum whose 'success' type is determined by the selection in the operation. 209 | 210 | Update your request's completion handler like this: 211 | 212 | ```swift 213 | ... completion: { result in 214 | switch result { 215 | case .success(let countries): 216 | for country in countries { 217 | print(country.name) 218 | } 219 | case .failure(let error): 220 | print(error) 221 | } 222 | } 223 | ``` 224 | 225 | Play around with the `countries` property passed into the `completion`. For example, you'll notice that, if you type `country.` inside the 226 | loop, you should get an autocomplete list for all the properties on `Country`. Let's run an experiment - update the selection on your 227 | `query` to this while keeping the same code in the `completion`, like this: 228 | 229 | ```swift 230 | client.perform(.query { 231 | $0.countryCount 232 | }, completion: { result in 233 | switch result { 234 | case .success(let countries): 235 | for country in countries { 236 | print(country.name) 237 | } 238 | case .failure(let error): 239 | print(error) 240 | } 241 | }) 242 | ``` 243 | 244 | ...and you'll get a compiler error. Since our selection changed, the type of result we get in our `completion` handler also changed - so, 245 | instead of `.success(let countries)` referring to an array of `Country` instances, it is now an integer (since `countryCount` returns an 246 | `Int`). Update your code to this, and it should all compile as expected: 247 | 248 | ```swift 249 | client.perform(.query { 250 | $0.countryCount 251 | }, completion: { result in 252 | switch result { 253 | case .success(let countryCount): 254 | print("There are \(countryCount) countries!") 255 | case .failure(let error): 256 | print(error) 257 | } 258 | }) 259 | ``` 260 | 261 | ### Passing arguments 262 | 263 | Passing arguments to fields is also very simple. We have one field in our API that can have arguments passed to it - our `country` field. 264 | Let's update our request code to look like this: 265 | 266 | ```swift 267 | client.perform(.query { 268 | $0.country { 269 | $0.name 270 | } 271 | }, completion: { _ in 272 | 273 | }) 274 | ``` 275 | ...and it won't compile. This is because, for fields that have arguments, they _must_ be passed (since this would result in an invalid query as 276 | well). To pass arguments, we just need to call that `country` selection with an `arguments` parameter with an instance of the field's 277 | associated `ArgumentsList` type (in this case, an instance of `CountryArgs`). That all looks like this: 278 | 279 | ```swift 280 | client.perform(.query { 281 | $0.country(arguments: .init(code: "CA")) { 282 | $0.name 283 | } 284 | }, completion: { _ in 285 | 286 | }) 287 | ``` 288 | 289 | ## Mutations 290 | 291 | Now, let's say our API wanted to add the ability to create new countries (creating new nations is as easy as an API call, after all). Events 292 | that create or update must be declared as a 'mutation' field. In Artemis, this is done in basically the same way as creating a query object - 293 | simply create a new class conforming to `Object`, then declare it on the schema. That all looks like this, updating our existing `MySchema`: 294 | 295 | ```swift 296 | final class MySchema: Schema { 297 | static let query = Query() 298 | static let mutation = Mutation() 299 | } 300 | 301 | final class Mutation: Object { 302 | 303 | } 304 | ``` 305 | 306 | Let's assume that our GraphQL schema for the mutation looks like this, where all the required fields of a country are provided: 307 | 308 | ``` 309 | type Mutation { 310 | createCountry(code: String!, name: String!, languages: [String!]!, continentCode: String!): Country! 311 | } 312 | 313 | ``` 314 | 315 | Then we'll just make up a quick `createCountry` field on our `Mutation`, along with an `ArgumentsList` type that contains all the 316 | arguments listed on the field: 317 | 318 | ```swift 319 | final class Mutation: Object { 320 | @Field("createCountry") 321 | var createCountry: (Country, CreateCountryArgs.Type) 322 | 323 | struct CreateCountryArgs: ArgumentsList { 324 | var code: String 325 | var name: String 326 | var languages: [String] 327 | var continentCode: String 328 | } 329 | } 330 | ``` 331 | 332 | ...and our mutation is ready to use in basically the same way as how we make queries - all we need to do is, instead of using the `.query` 333 | function to build our operation, we use `.mutation`, calling fields from our schema's `mutation` type: 334 | 335 | ```swift 336 | client.perform(.mutation { 337 | $0.createCountry(arguments: .init(code: "GV", name: "Genovia", languages: ["fr", "it", "en"], continentCode: "eu")) { 338 | $0.name 339 | } 340 | }, completion: { _ in 341 | 342 | }) 343 | ``` 344 | 345 | ### Input objects 346 | 347 | Many GraphQL APIs will use input objects rather than long lists of arguments, especially if arguments are shared between fields. Let's 348 | imagine our countries API updated its schema so that the `createCountry` field took an input object instead of a long arguments list: 349 | 350 | ``` 351 | input CountryInput { 352 | code: String! 353 | name: String! 354 | languages: [String!]! 355 | continentCode: String! 356 | } 357 | 358 | type Mutation { 359 | createCountry(input: CountryInput!): Country! 360 | } 361 | 362 | ``` 363 | 364 | This is supported in Artemis by defining types that conform to `Input` (which, like `ArgumentsList`, just requires that the type conform to 365 | `Encodable`). This means that, most of the time, we'll just define a struct with all the properties of the input (that way, we get the 366 | memberwise initializer for free!). The updated mutation API might then look like this: 367 | 368 | ```swift 369 | struct CountryInput: Input { 370 | var code: String 371 | var name: String 372 | var languages: [String] 373 | var continentCode: String 374 | } 375 | 376 | final class Mutation: Object { 377 | @Field("createCountry") 378 | var createCountry: (Country, CreateCountryArgs.Type) 379 | 380 | struct CreateCountryArgs: ArgumentsList { 381 | var input: CountryInput 382 | } 383 | } 384 | ``` 385 | 386 | Calling our `createCountry` field can then look like this: 387 | 388 | ```swift 389 | let input = CountryInput(code: "GV", name: "Genovia", languages: ["fr", "it", "en"], continentCode: "eu") 390 | 391 | client.perform(.mutation { 392 | $0.createCountry(input: .init(input: input)) { 393 | $0.name 394 | } 395 | }, completion: { _ in 396 | 397 | }) 398 | ``` 399 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TestInterface1/TestInterface1_Int_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | @testable import Artemis 6 | 7 | // MARK: - Tests to ensure single selections of Int and [Int] render as expected 8 | 9 | extension TestInterface1_Int_TypeTests { 10 | func testSingle() { 11 | let query: _Operation = .query { 12 | $0.i1_int 13 | } 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "i1_int": 123 18 | } 19 | } 20 | """.utf8) 21 | 22 | XCTAssertEqual(query.render(), "{i1_int}") 23 | let res = try? query.createResult(from: response) 24 | XCTAssertEqual(res, 123) 25 | } 26 | 27 | func testSingleArgs() { 28 | let query: _Operation = .query { 29 | $0.i1_intArgs(arguments: .testDefault) 30 | } 31 | let response = Data(""" 32 | { 33 | "data": { 34 | "i1_intArgs": 123 35 | } 36 | } 37 | """.utf8) 38 | 39 | XCTAssertEqual(query.render(), "{i1_intArgs\(testArgs)}") 40 | let res = try? query.createResult(from: response) 41 | XCTAssertEqual(res, 123) 42 | } 43 | 44 | func testArray() { 45 | let query: _Operation = .query { 46 | $0.i1_ints 47 | } 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "i1_ints": [123, 321] 52 | } 53 | } 54 | """.utf8) 55 | 56 | XCTAssertEqual(query.render(), "{i1_ints}") 57 | let res = try? query.createResult(from: response) 58 | XCTAssertEqual(res?.count, 2) 59 | XCTAssertEqual(res?[safe: 0], 123) 60 | XCTAssertEqual(res?[safe: 1], 321) 61 | } 62 | 63 | func testOptional() { 64 | let query: _Operation = .query { 65 | $0.i1_intOptional 66 | } 67 | let response = Data(""" 68 | { 69 | "data": { 70 | "i1_intOptional": 123 71 | } 72 | } 73 | """.utf8) 74 | 75 | XCTAssertEqual(query.render(), "{i1_intOptional}") 76 | let res = try? query.createResult(from: response) 77 | XCTAssertEqual(res, 123) 78 | } 79 | } 80 | 81 | // MARK: - Tests to ensure single selections of Int and [Int] with aliases render as expected 82 | 83 | extension TestInterface1_Int_TypeTests { 84 | func testSingleAlias() { 85 | let query: _Operation = .query { 86 | $0.i1_int(alias: "alias") 87 | } 88 | let response = Data(""" 89 | { 90 | "data": { 91 | "alias": 123 92 | } 93 | } 94 | """.utf8) 95 | 96 | XCTAssertEqual(query.render(), "{alias:i1_int}") 97 | let res = try? query.createResult(from: response) 98 | XCTAssertEqual(res, 123) 99 | } 100 | 101 | func testSingleArgsAlias() { 102 | let query: _Operation = .query { 103 | $0.i1_intArgs(alias: "alias", arguments: .testDefault) 104 | } 105 | let response = Data(""" 106 | { 107 | "data": { 108 | "alias": 123 109 | } 110 | } 111 | """.utf8) 112 | 113 | XCTAssertEqual(query.render(), "{alias:i1_intArgs\(testArgs)}") 114 | let res = try? query.createResult(from: response) 115 | XCTAssertEqual(res, 123) 116 | } 117 | } 118 | 119 | // MARK: - Tests to ensure selections render as expected on selections of Int and [Int] on an Object 120 | 121 | extension TestInterface1_Int_TypeTests { 122 | func testSingleOnObject() { 123 | let query: _Operation> = .query { 124 | $0.testObject { 125 | $0.i1_int 126 | } 127 | } 128 | let response = Data(""" 129 | { 130 | "data": { 131 | "testObject": { 132 | "i1_int": 123 133 | } 134 | } 135 | } 136 | """.utf8) 137 | 138 | XCTAssertEqual(query.render(), "{testObject{i1_int}}") 139 | let res: Partial? = try? query.createResult(from: response) 140 | XCTAssertEqual(res?.values.count, 1) 141 | XCTAssertEqual(res?.i1_int, 123) 142 | } 143 | 144 | func testSingleArgsOnObject() { 145 | let query: _Operation> = .query { 146 | $0.testObject { 147 | $0.i1_intArgs(arguments: .testDefault) 148 | } 149 | } 150 | let response = Data(""" 151 | { 152 | "data": { 153 | "testObject": { 154 | "i1_intArgs": 123 155 | } 156 | } 157 | } 158 | """.utf8) 159 | 160 | XCTAssertEqual(query.render(), "{testObject{i1_intArgs\(testArgs)}}") 161 | let res: Partial? = try? query.createResult(from: response) 162 | XCTAssertEqual(res?.values.count, 1) 163 | XCTAssertEqual(res?.i1_intArgs, 123) 164 | } 165 | 166 | func testArrayOnObject() { 167 | let query: _Operation> = .query { 168 | $0.testObject { 169 | $0.i1_ints 170 | } 171 | } 172 | let response = Data(""" 173 | { 174 | "data": { 175 | "testObject": { 176 | "i1_ints": [123, 321] 177 | } 178 | } 179 | } 180 | """.utf8) 181 | 182 | XCTAssertEqual(query.render(), "{testObject{i1_ints}}") 183 | let res: Partial? = try? query.createResult(from: response) 184 | XCTAssertEqual(res?.values.count, 1) 185 | XCTAssertEqual(res?.i1_ints?.count, 2) 186 | XCTAssertEqual(res?.i1_ints?[safe: 0], 123) 187 | XCTAssertEqual(res?.i1_ints?[safe: 1], 321) 188 | } 189 | 190 | func testOptionalOnObject() { 191 | let query: _Operation> = .query { 192 | $0.testObject { 193 | $0.i1_intOptional 194 | } 195 | } 196 | let response = Data(""" 197 | { 198 | "data": { 199 | "testObject": { 200 | "i1_intOptional": 123 201 | } 202 | } 203 | } 204 | """.utf8) 205 | 206 | XCTAssertEqual(query.render(), "{testObject{i1_intOptional}}") 207 | let res: Partial? = try? query.createResult(from: response) 208 | XCTAssertEqual(res?.values.count, 1) 209 | XCTAssertEqual(res?.i1_intOptional, 123) 210 | } 211 | 212 | } 213 | 214 | // MARK: - Tests to ensure an alias of Int and [Int] on an Object can be used to pull values out of a result 215 | 216 | extension TestInterface1_Int_TypeTests { 217 | func testSingleAliasOnObject() throws { 218 | let query: _Operation> = .query { 219 | $0.testObject { 220 | $0.i1_int(alias: "alias") 221 | } 222 | } 223 | let response = Data(""" 224 | { 225 | "data": { 226 | "testObject": { 227 | "alias": 123 228 | } 229 | } 230 | } 231 | """.utf8) 232 | 233 | XCTAssertEqual(query.render(), "{testObject{alias:i1_int}}") 234 | let res: Partial? = try? query.createResult(from: response) 235 | XCTAssertEqual(res?.values.count, 1) 236 | let aliased = res?.i1_int(alias: "alias") 237 | XCTAssertEqual(aliased, 123) 238 | } 239 | 240 | func testSingleArgsAliasOnObject() throws { 241 | let query: _Operation> = .query { 242 | $0.testObject { 243 | $0.i1_intArgs(alias: "alias", arguments: .testDefault) 244 | } 245 | } 246 | let response = Data(""" 247 | { 248 | "data": { 249 | "testObject": { 250 | "alias": 123 251 | } 252 | } 253 | } 254 | """.utf8) 255 | 256 | XCTAssertEqual(query.render(), "{testObject{alias:i1_intArgs\(testArgs)}}") 257 | let res: Partial? = try? query.createResult(from: response) 258 | XCTAssertEqual(res?.values.count, 1) 259 | let aliased = res?.i1_intArgs(alias: "alias") 260 | XCTAssertEqual(aliased, 123) 261 | } 262 | 263 | func testArrayAliasOnObject() throws { 264 | let query: _Operation> = .query { 265 | $0.testObject { 266 | $0.i1_ints(alias: "alias") 267 | } 268 | } 269 | let response = Data(""" 270 | { 271 | "data": { 272 | "testObject": { 273 | "alias": [123, 321] 274 | } 275 | } 276 | } 277 | """.utf8) 278 | 279 | XCTAssertEqual(query.render(), "{testObject{alias:i1_ints}}") 280 | let res: Partial? = try? query.createResult(from: response) 281 | XCTAssertEqual(res?.values.count, 1) 282 | let aliased = res?.i1_ints(alias: "alias") 283 | XCTAssertEqual(aliased?[safe: 0], 123) 284 | XCTAssertEqual(aliased?[safe: 1], 321) 285 | } 286 | 287 | } 288 | 289 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation 290 | 291 | extension TestInterface1_Int_TypeTests { 292 | func testSingleOnFragment() { 293 | let fragment = Fragment("fragName", on: Query.self) { 294 | $0.i1_int 295 | } 296 | let query: _Operation = .query { 297 | fragment 298 | } 299 | let response = Data(""" 300 | { 301 | "data": { 302 | "i1_int": 123 303 | } 304 | } 305 | """.utf8) 306 | 307 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i1_int}") 308 | let res = try? query.createResult(from: response) 309 | XCTAssertEqual(res, 123) 310 | } 311 | 312 | func testSingleArgsOnFragment() { 313 | let fragment = Fragment("fragName", on: Query.self) { 314 | $0.i1_intArgs(arguments: .testDefault) 315 | } 316 | let query: _Operation = .query { 317 | fragment 318 | } 319 | let response = Data(""" 320 | { 321 | "data": { 322 | "i1_intArgs": 123 323 | } 324 | } 325 | """.utf8) 326 | 327 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i1_intArgs\(testArgs)}") 328 | let res = try? query.createResult(from: response) 329 | XCTAssertEqual(res, 123) 330 | } 331 | 332 | func testArrayOnFragment() { 333 | let fragment = Fragment("fragName", on: Query.self) { 334 | $0.i1_ints 335 | } 336 | let query: _Operation = .query { 337 | fragment 338 | } 339 | let response = Data(""" 340 | { 341 | "data": { 342 | "i1_ints": [123, 321] 343 | } 344 | } 345 | """.utf8) 346 | 347 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i1_ints}") 348 | let res = try? query.createResult(from: response) 349 | XCTAssertEqual(res?.count, 2) 350 | XCTAssertEqual(res?[safe: 0], 123) 351 | XCTAssertEqual(res?[safe: 1], 321) 352 | } 353 | 354 | func testOptionalOnFragment() { 355 | let fragment = Fragment("fragName", on: Query.self) { 356 | $0.i1_intOptional 357 | } 358 | let query: _Operation = .query { 359 | fragment 360 | } 361 | let response = Data(""" 362 | { 363 | "data": { 364 | "i1_intOptional": 123 365 | } 366 | } 367 | """.utf8) 368 | 369 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i1_intOptional}") 370 | let res = try? query.createResult(from: response) 371 | XCTAssertEqual(res, 123) 372 | } 373 | } 374 | 375 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation with aliases 376 | 377 | extension TestInterface1_Int_TypeTests { 378 | func testSingleAliasOnFragment() { 379 | let fragment = Fragment("fragName", on: Query.self) { 380 | $0.i1_int(alias: "alias") 381 | } 382 | let query: _Operation = .query { 383 | fragment 384 | } 385 | let response = Data(""" 386 | { 387 | "data": { 388 | "alias": 123 389 | } 390 | } 391 | """.utf8) 392 | 393 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i1_int}") 394 | let res = try? query.createResult(from: response) 395 | XCTAssertEqual(res, 123) 396 | } 397 | 398 | func testSingleArgsAliasOnFragment() { 399 | let fragment = Fragment("fragName", on: Query.self) { 400 | $0.i1_intArgs(alias: "alias", arguments: .testDefault) 401 | } 402 | let query: _Operation = .query { 403 | fragment 404 | } 405 | let response = Data(""" 406 | { 407 | "data": { 408 | "alias": 123 409 | } 410 | } 411 | """.utf8) 412 | 413 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i1_intArgs\(testArgs)}") 414 | let res = try? query.createResult(from: response) 415 | XCTAssertEqual(res, 123) 416 | } 417 | } 418 | 419 | 420 | // MARK: - Tests to ensure fragments on TestInterface1 can be used on selections of Int or [Int] 421 | 422 | extension TestInterface1_Int_TypeTests { 423 | func testSingleOnTestInterface1Fragment() { 424 | let fragment = Fragment("fragName", on: TestInterface1.self) { 425 | $0.i1_int 426 | } 427 | let query: _Operation = .query { 428 | $0.testObject { 429 | fragment 430 | } 431 | } 432 | let response = Data(""" 433 | { 434 | "data": { 435 | "testObject": { 436 | "i1_int": 123 437 | } 438 | } 439 | } 440 | """.utf8) 441 | 442 | XCTAssertEqual(query.render(), "{testObject{...fragName}},fragment fragName on TestInterface1{i1_int}") 443 | let res: Partial? = try? query.createResult(from: response) 444 | XCTAssertEqual(res?.values.count, 1) 445 | XCTAssertEqual(res?.i1_int, 123) 446 | } 447 | 448 | func testArrayOnObjectFragment() { 449 | let fragment = Fragment("fragName", on: TestInterface1.self) { 450 | $0.i1_int 451 | } 452 | let query: _Operation = .query { 453 | $0.testObjects { 454 | fragment 455 | } 456 | } 457 | let response = Data(""" 458 | { 459 | "data": { 460 | "testObjects": [ 461 | { "i1_int": 123 }, 462 | { "i1_int": 321 } 463 | ] 464 | } 465 | } 466 | """.utf8) 467 | 468 | XCTAssertEqual(query.render(), "{testObjects{...fragName}},fragment fragName on TestInterface1{i1_int}") 469 | let res: [Partial]? = try? query.createResult(from: response) 470 | XCTAssertEqual(res?.count, 2) 471 | XCTAssertEqual(res?[safe: 0]?.i1_int, 123) 472 | XCTAssertEqual(res?[safe: 1]?.i1_int, 321) 473 | } 474 | 475 | func testOptionalOnObjectFragment() { 476 | let fragment = Fragment("fragName", on: TestInterface1.self) { 477 | $0.i1_int 478 | } 479 | let query: _Operation = .query { 480 | $0.testObjectOptional { 481 | fragment 482 | } 483 | } 484 | let response = Data(""" 485 | { 486 | "data": { 487 | "testObjectOptional": { 488 | "i1_int": 123 489 | } 490 | } 491 | } 492 | """.utf8) 493 | 494 | XCTAssertEqual(query.render(), "{testObjectOptional{...fragName}},fragment fragName on TestInterface1{i1_int}") 495 | let res: Partial? = try? query.createResult(from: response) 496 | XCTAssertEqual(res?.values.count, 1) 497 | XCTAssertEqual(res?.i1_int, 123) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TestInterface2/TestInterface2_Int_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | @testable import Artemis 6 | 7 | // MARK: - Tests to ensure single selections of Int and [Int] render as expected 8 | 9 | extension TestInterface2_Int_TypeTests { 10 | func testSingle() { 11 | let query: _Operation = .query { 12 | $0.i2_int 13 | } 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "i2_int": 123 18 | } 19 | } 20 | """.utf8) 21 | 22 | XCTAssertEqual(query.render(), "{i2_int}") 23 | let res = try? query.createResult(from: response) 24 | XCTAssertEqual(res, 123) 25 | } 26 | 27 | func testSingleArgs() { 28 | let query: _Operation = .query { 29 | $0.i2_intArgs(arguments: .testDefault) 30 | } 31 | let response = Data(""" 32 | { 33 | "data": { 34 | "i2_intArgs": 123 35 | } 36 | } 37 | """.utf8) 38 | 39 | XCTAssertEqual(query.render(), "{i2_intArgs\(testArgs)}") 40 | let res = try? query.createResult(from: response) 41 | XCTAssertEqual(res, 123) 42 | } 43 | 44 | func testArray() { 45 | let query: _Operation = .query { 46 | $0.i2_ints 47 | } 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "i2_ints": [123, 321] 52 | } 53 | } 54 | """.utf8) 55 | 56 | XCTAssertEqual(query.render(), "{i2_ints}") 57 | let res = try? query.createResult(from: response) 58 | XCTAssertEqual(res?.count, 2) 59 | XCTAssertEqual(res?[safe: 0], 123) 60 | XCTAssertEqual(res?[safe: 1], 321) 61 | } 62 | 63 | func testOptional() { 64 | let query: _Operation = .query { 65 | $0.i2_intOptional 66 | } 67 | let response = Data(""" 68 | { 69 | "data": { 70 | "i2_intOptional": 123 71 | } 72 | } 73 | """.utf8) 74 | 75 | XCTAssertEqual(query.render(), "{i2_intOptional}") 76 | let res = try? query.createResult(from: response) 77 | XCTAssertEqual(res, 123) 78 | } 79 | } 80 | 81 | // MARK: - Tests to ensure single selections of Int and [Int] with aliases render as expected 82 | 83 | extension TestInterface2_Int_TypeTests { 84 | func testSingleAlias() { 85 | let query: _Operation = .query { 86 | $0.i2_int(alias: "alias") 87 | } 88 | let response = Data(""" 89 | { 90 | "data": { 91 | "alias": 123 92 | } 93 | } 94 | """.utf8) 95 | 96 | XCTAssertEqual(query.render(), "{alias:i2_int}") 97 | let res = try? query.createResult(from: response) 98 | XCTAssertEqual(res, 123) 99 | } 100 | 101 | func testSingleArgsAlias() { 102 | let query: _Operation = .query { 103 | $0.i2_intArgs(alias: "alias", arguments: .testDefault) 104 | } 105 | let response = Data(""" 106 | { 107 | "data": { 108 | "alias": 123 109 | } 110 | } 111 | """.utf8) 112 | 113 | XCTAssertEqual(query.render(), "{alias:i2_intArgs\(testArgs)}") 114 | let res = try? query.createResult(from: response) 115 | XCTAssertEqual(res, 123) 116 | } 117 | } 118 | 119 | // MARK: - Tests to ensure selections render as expected on selections of Int and [Int] on an Object 120 | 121 | extension TestInterface2_Int_TypeTests { 122 | func testSingleOnObject() { 123 | let query: _Operation> = .query { 124 | $0.testObject { 125 | $0.i2_int 126 | } 127 | } 128 | let response = Data(""" 129 | { 130 | "data": { 131 | "testObject": { 132 | "i2_int": 123 133 | } 134 | } 135 | } 136 | """.utf8) 137 | 138 | XCTAssertEqual(query.render(), "{testObject{i2_int}}") 139 | let res: Partial? = try? query.createResult(from: response) 140 | XCTAssertEqual(res?.values.count, 1) 141 | XCTAssertEqual(res?.i2_int, 123) 142 | } 143 | 144 | func testSingleArgsOnObject() { 145 | let query: _Operation> = .query { 146 | $0.testObject { 147 | $0.i2_intArgs(arguments: .testDefault) 148 | } 149 | } 150 | let response = Data(""" 151 | { 152 | "data": { 153 | "testObject": { 154 | "i2_intArgs": 123 155 | } 156 | } 157 | } 158 | """.utf8) 159 | 160 | XCTAssertEqual(query.render(), "{testObject{i2_intArgs\(testArgs)}}") 161 | let res: Partial? = try? query.createResult(from: response) 162 | XCTAssertEqual(res?.values.count, 1) 163 | XCTAssertEqual(res?.i2_intArgs, 123) 164 | } 165 | 166 | func testArrayOnObject() { 167 | let query: _Operation> = .query { 168 | $0.testObject { 169 | $0.i2_ints 170 | } 171 | } 172 | let response = Data(""" 173 | { 174 | "data": { 175 | "testObject": { 176 | "i2_ints": [123, 321] 177 | } 178 | } 179 | } 180 | """.utf8) 181 | 182 | XCTAssertEqual(query.render(), "{testObject{i2_ints}}") 183 | let res: Partial? = try? query.createResult(from: response) 184 | XCTAssertEqual(res?.values.count, 1) 185 | XCTAssertEqual(res?.i2_ints?.count, 2) 186 | XCTAssertEqual(res?.i2_ints?[safe: 0], 123) 187 | XCTAssertEqual(res?.i2_ints?[safe: 1], 321) 188 | } 189 | 190 | func testOptionalOnObject() { 191 | let query: _Operation> = .query { 192 | $0.testObject { 193 | $0.i2_intOptional 194 | } 195 | } 196 | let response = Data(""" 197 | { 198 | "data": { 199 | "testObject": { 200 | "i2_intOptional": 123 201 | } 202 | } 203 | } 204 | """.utf8) 205 | 206 | XCTAssertEqual(query.render(), "{testObject{i2_intOptional}}") 207 | let res: Partial? = try? query.createResult(from: response) 208 | XCTAssertEqual(res?.values.count, 1) 209 | XCTAssertEqual(res?.i2_intOptional, 123) 210 | } 211 | 212 | } 213 | 214 | // MARK: - Tests to ensure an alias of Int and [Int] on an Object can be used to pull values out of a result 215 | 216 | extension TestInterface2_Int_TypeTests { 217 | func testSingleAliasOnObject() throws { 218 | let query: _Operation> = .query { 219 | $0.testObject { 220 | $0.i2_int(alias: "alias") 221 | } 222 | } 223 | let response = Data(""" 224 | { 225 | "data": { 226 | "testObject": { 227 | "alias": 123 228 | } 229 | } 230 | } 231 | """.utf8) 232 | 233 | XCTAssertEqual(query.render(), "{testObject{alias:i2_int}}") 234 | let res: Partial? = try? query.createResult(from: response) 235 | XCTAssertEqual(res?.values.count, 1) 236 | let aliased = res?.i2_int(alias: "alias") 237 | XCTAssertEqual(aliased, 123) 238 | } 239 | 240 | func testSingleArgsAliasOnObject() throws { 241 | let query: _Operation> = .query { 242 | $0.testObject { 243 | $0.i2_intArgs(alias: "alias", arguments: .testDefault) 244 | } 245 | } 246 | let response = Data(""" 247 | { 248 | "data": { 249 | "testObject": { 250 | "alias": 123 251 | } 252 | } 253 | } 254 | """.utf8) 255 | 256 | XCTAssertEqual(query.render(), "{testObject{alias:i2_intArgs\(testArgs)}}") 257 | let res: Partial? = try? query.createResult(from: response) 258 | XCTAssertEqual(res?.values.count, 1) 259 | let aliased = res?.i2_intArgs(alias: "alias") 260 | XCTAssertEqual(aliased, 123) 261 | } 262 | 263 | func testArrayAliasOnObject() throws { 264 | let query: _Operation> = .query { 265 | $0.testObject { 266 | $0.i2_ints(alias: "alias") 267 | } 268 | } 269 | let response = Data(""" 270 | { 271 | "data": { 272 | "testObject": { 273 | "alias": [123, 321] 274 | } 275 | } 276 | } 277 | """.utf8) 278 | 279 | XCTAssertEqual(query.render(), "{testObject{alias:i2_ints}}") 280 | let res: Partial? = try? query.createResult(from: response) 281 | XCTAssertEqual(res?.values.count, 1) 282 | let aliased = res?.i2_ints(alias: "alias") 283 | XCTAssertEqual(aliased?[safe: 0], 123) 284 | XCTAssertEqual(aliased?[safe: 1], 321) 285 | } 286 | 287 | } 288 | 289 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation 290 | 291 | extension TestInterface2_Int_TypeTests { 292 | func testSingleOnFragment() { 293 | let fragment = Fragment("fragName", on: Query.self) { 294 | $0.i2_int 295 | } 296 | let query: _Operation = .query { 297 | fragment 298 | } 299 | let response = Data(""" 300 | { 301 | "data": { 302 | "i2_int": 123 303 | } 304 | } 305 | """.utf8) 306 | 307 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i2_int}") 308 | let res = try? query.createResult(from: response) 309 | XCTAssertEqual(res, 123) 310 | } 311 | 312 | func testSingleArgsOnFragment() { 313 | let fragment = Fragment("fragName", on: Query.self) { 314 | $0.i2_intArgs(arguments: .testDefault) 315 | } 316 | let query: _Operation = .query { 317 | fragment 318 | } 319 | let response = Data(""" 320 | { 321 | "data": { 322 | "i2_intArgs": 123 323 | } 324 | } 325 | """.utf8) 326 | 327 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i2_intArgs\(testArgs)}") 328 | let res = try? query.createResult(from: response) 329 | XCTAssertEqual(res, 123) 330 | } 331 | 332 | func testArrayOnFragment() { 333 | let fragment = Fragment("fragName", on: Query.self) { 334 | $0.i2_ints 335 | } 336 | let query: _Operation = .query { 337 | fragment 338 | } 339 | let response = Data(""" 340 | { 341 | "data": { 342 | "i2_ints": [123, 321] 343 | } 344 | } 345 | """.utf8) 346 | 347 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i2_ints}") 348 | let res = try? query.createResult(from: response) 349 | XCTAssertEqual(res?.count, 2) 350 | XCTAssertEqual(res?[safe: 0], 123) 351 | XCTAssertEqual(res?[safe: 1], 321) 352 | } 353 | 354 | func testOptionalOnFragment() { 355 | let fragment = Fragment("fragName", on: Query.self) { 356 | $0.i2_intOptional 357 | } 358 | let query: _Operation = .query { 359 | fragment 360 | } 361 | let response = Data(""" 362 | { 363 | "data": { 364 | "i2_intOptional": 123 365 | } 366 | } 367 | """.utf8) 368 | 369 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i2_intOptional}") 370 | let res = try? query.createResult(from: response) 371 | XCTAssertEqual(res, 123) 372 | } 373 | } 374 | 375 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation with aliases 376 | 377 | extension TestInterface2_Int_TypeTests { 378 | func testSingleAliasOnFragment() { 379 | let fragment = Fragment("fragName", on: Query.self) { 380 | $0.i2_int(alias: "alias") 381 | } 382 | let query: _Operation = .query { 383 | fragment 384 | } 385 | let response = Data(""" 386 | { 387 | "data": { 388 | "alias": 123 389 | } 390 | } 391 | """.utf8) 392 | 393 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i2_int}") 394 | let res = try? query.createResult(from: response) 395 | XCTAssertEqual(res, 123) 396 | } 397 | 398 | func testSingleArgsAliasOnFragment() { 399 | let fragment = Fragment("fragName", on: Query.self) { 400 | $0.i2_intArgs(alias: "alias", arguments: .testDefault) 401 | } 402 | let query: _Operation = .query { 403 | fragment 404 | } 405 | let response = Data(""" 406 | { 407 | "data": { 408 | "alias": 123 409 | } 410 | } 411 | """.utf8) 412 | 413 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i2_intArgs\(testArgs)}") 414 | let res = try? query.createResult(from: response) 415 | XCTAssertEqual(res, 123) 416 | } 417 | } 418 | 419 | 420 | // MARK: - Tests to ensure fragments on TestInterface2 can be used on selections of Int or [Int] 421 | 422 | extension TestInterface2_Int_TypeTests { 423 | func testSingleOnTestInterface2Fragment() { 424 | let fragment = Fragment("fragName", on: TestInterface2.self) { 425 | $0.i2_int 426 | } 427 | let query: _Operation = .query { 428 | $0.testObject { 429 | fragment 430 | } 431 | } 432 | let response = Data(""" 433 | { 434 | "data": { 435 | "testObject": { 436 | "i2_int": 123 437 | } 438 | } 439 | } 440 | """.utf8) 441 | 442 | XCTAssertEqual(query.render(), "{testObject{...fragName}},fragment fragName on TestInterface2{i2_int}") 443 | let res: Partial? = try? query.createResult(from: response) 444 | XCTAssertEqual(res?.values.count, 1) 445 | XCTAssertEqual(res?.i2_int, 123) 446 | } 447 | 448 | func testArrayOnObjectFragment() { 449 | let fragment = Fragment("fragName", on: TestInterface2.self) { 450 | $0.i2_int 451 | } 452 | let query: _Operation = .query { 453 | $0.testObjects { 454 | fragment 455 | } 456 | } 457 | let response = Data(""" 458 | { 459 | "data": { 460 | "testObjects": [ 461 | { "i2_int": 123 }, 462 | { "i2_int": 321 } 463 | ] 464 | } 465 | } 466 | """.utf8) 467 | 468 | XCTAssertEqual(query.render(), "{testObjects{...fragName}},fragment fragName on TestInterface2{i2_int}") 469 | let res: [Partial]? = try? query.createResult(from: response) 470 | XCTAssertEqual(res?.count, 2) 471 | XCTAssertEqual(res?[safe: 0]?.i2_int, 123) 472 | XCTAssertEqual(res?[safe: 1]?.i2_int, 321) 473 | } 474 | 475 | func testOptionalOnObjectFragment() { 476 | let fragment = Fragment("fragName", on: TestInterface2.self) { 477 | $0.i2_int 478 | } 479 | let query: _Operation = .query { 480 | $0.testObjectOptional { 481 | fragment 482 | } 483 | } 484 | let response = Data(""" 485 | { 486 | "data": { 487 | "testObjectOptional": { 488 | "i2_int": 123 489 | } 490 | } 491 | } 492 | """.utf8) 493 | 494 | XCTAssertEqual(query.render(), "{testObjectOptional{...fragName}},fragment fragName on TestInterface2{i2_int}") 495 | let res: Partial? = try? query.createResult(from: response) 496 | XCTAssertEqual(res?.values.count, 1) 497 | XCTAssertEqual(res?.i2_int, 123) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TestInterface3/TestInterface3_Int_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | @testable import Artemis 6 | 7 | // MARK: - Tests to ensure single selections of Int and [Int] render as expected 8 | 9 | extension TestInterface3_Int_TypeTests { 10 | func testSingle() { 11 | let query: _Operation = .query { 12 | $0.i3_int 13 | } 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "i3_int": 123 18 | } 19 | } 20 | """.utf8) 21 | 22 | XCTAssertEqual(query.render(), "{i3_int}") 23 | let res = try? query.createResult(from: response) 24 | XCTAssertEqual(res, 123) 25 | } 26 | 27 | func testSingleArgs() { 28 | let query: _Operation = .query { 29 | $0.i3_intArgs(arguments: .testDefault) 30 | } 31 | let response = Data(""" 32 | { 33 | "data": { 34 | "i3_intArgs": 123 35 | } 36 | } 37 | """.utf8) 38 | 39 | XCTAssertEqual(query.render(), "{i3_intArgs\(testArgs)}") 40 | let res = try? query.createResult(from: response) 41 | XCTAssertEqual(res, 123) 42 | } 43 | 44 | func testArray() { 45 | let query: _Operation = .query { 46 | $0.i3_ints 47 | } 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "i3_ints": [123, 321] 52 | } 53 | } 54 | """.utf8) 55 | 56 | XCTAssertEqual(query.render(), "{i3_ints}") 57 | let res = try? query.createResult(from: response) 58 | XCTAssertEqual(res?.count, 2) 59 | XCTAssertEqual(res?[safe: 0], 123) 60 | XCTAssertEqual(res?[safe: 1], 321) 61 | } 62 | 63 | func testOptional() { 64 | let query: _Operation = .query { 65 | $0.i3_intOptional 66 | } 67 | let response = Data(""" 68 | { 69 | "data": { 70 | "i3_intOptional": 123 71 | } 72 | } 73 | """.utf8) 74 | 75 | XCTAssertEqual(query.render(), "{i3_intOptional}") 76 | let res = try? query.createResult(from: response) 77 | XCTAssertEqual(res, 123) 78 | } 79 | } 80 | 81 | // MARK: - Tests to ensure single selections of Int and [Int] with aliases render as expected 82 | 83 | extension TestInterface3_Int_TypeTests { 84 | func testSingleAlias() { 85 | let query: _Operation = .query { 86 | $0.i3_int(alias: "alias") 87 | } 88 | let response = Data(""" 89 | { 90 | "data": { 91 | "alias": 123 92 | } 93 | } 94 | """.utf8) 95 | 96 | XCTAssertEqual(query.render(), "{alias:i3_int}") 97 | let res = try? query.createResult(from: response) 98 | XCTAssertEqual(res, 123) 99 | } 100 | 101 | func testSingleArgsAlias() { 102 | let query: _Operation = .query { 103 | $0.i3_intArgs(alias: "alias", arguments: .testDefault) 104 | } 105 | let response = Data(""" 106 | { 107 | "data": { 108 | "alias": 123 109 | } 110 | } 111 | """.utf8) 112 | 113 | XCTAssertEqual(query.render(), "{alias:i3_intArgs\(testArgs)}") 114 | let res = try? query.createResult(from: response) 115 | XCTAssertEqual(res, 123) 116 | } 117 | } 118 | 119 | // MARK: - Tests to ensure selections render as expected on selections of Int and [Int] on an Object 120 | 121 | extension TestInterface3_Int_TypeTests { 122 | func testSingleOnObject() { 123 | let query: _Operation> = .query { 124 | $0.testObject { 125 | $0.i3_int 126 | } 127 | } 128 | let response = Data(""" 129 | { 130 | "data": { 131 | "testObject": { 132 | "i3_int": 123 133 | } 134 | } 135 | } 136 | """.utf8) 137 | 138 | XCTAssertEqual(query.render(), "{testObject{i3_int}}") 139 | let res: Partial? = try? query.createResult(from: response) 140 | XCTAssertEqual(res?.values.count, 1) 141 | XCTAssertEqual(res?.i3_int, 123) 142 | } 143 | 144 | func testSingleArgsOnObject() { 145 | let query: _Operation> = .query { 146 | $0.testObject { 147 | $0.i3_intArgs(arguments: .testDefault) 148 | } 149 | } 150 | let response = Data(""" 151 | { 152 | "data": { 153 | "testObject": { 154 | "i3_intArgs": 123 155 | } 156 | } 157 | } 158 | """.utf8) 159 | 160 | XCTAssertEqual(query.render(), "{testObject{i3_intArgs\(testArgs)}}") 161 | let res: Partial? = try? query.createResult(from: response) 162 | XCTAssertEqual(res?.values.count, 1) 163 | XCTAssertEqual(res?.i3_intArgs, 123) 164 | } 165 | 166 | func testArrayOnObject() { 167 | let query: _Operation> = .query { 168 | $0.testObject { 169 | $0.i3_ints 170 | } 171 | } 172 | let response = Data(""" 173 | { 174 | "data": { 175 | "testObject": { 176 | "i3_ints": [123, 321] 177 | } 178 | } 179 | } 180 | """.utf8) 181 | 182 | XCTAssertEqual(query.render(), "{testObject{i3_ints}}") 183 | let res: Partial? = try? query.createResult(from: response) 184 | XCTAssertEqual(res?.values.count, 1) 185 | XCTAssertEqual(res?.i3_ints?.count, 2) 186 | XCTAssertEqual(res?.i3_ints?[safe: 0], 123) 187 | XCTAssertEqual(res?.i3_ints?[safe: 1], 321) 188 | } 189 | 190 | func testOptionalOnObject() { 191 | let query: _Operation> = .query { 192 | $0.testObject { 193 | $0.i3_intOptional 194 | } 195 | } 196 | let response = Data(""" 197 | { 198 | "data": { 199 | "testObject": { 200 | "i3_intOptional": 123 201 | } 202 | } 203 | } 204 | """.utf8) 205 | 206 | XCTAssertEqual(query.render(), "{testObject{i3_intOptional}}") 207 | let res: Partial? = try? query.createResult(from: response) 208 | XCTAssertEqual(res?.values.count, 1) 209 | XCTAssertEqual(res?.i3_intOptional, 123) 210 | } 211 | 212 | } 213 | 214 | // MARK: - Tests to ensure an alias of Int and [Int] on an Object can be used to pull values out of a result 215 | 216 | extension TestInterface3_Int_TypeTests { 217 | func testSingleAliasOnObject() throws { 218 | let query: _Operation> = .query { 219 | $0.testObject { 220 | $0.i3_int(alias: "alias") 221 | } 222 | } 223 | let response = Data(""" 224 | { 225 | "data": { 226 | "testObject": { 227 | "alias": 123 228 | } 229 | } 230 | } 231 | """.utf8) 232 | 233 | XCTAssertEqual(query.render(), "{testObject{alias:i3_int}}") 234 | let res: Partial? = try? query.createResult(from: response) 235 | XCTAssertEqual(res?.values.count, 1) 236 | let aliased = res?.i3_int(alias: "alias") 237 | XCTAssertEqual(aliased, 123) 238 | } 239 | 240 | func testSingleArgsAliasOnObject() throws { 241 | let query: _Operation> = .query { 242 | $0.testObject { 243 | $0.i3_intArgs(alias: "alias", arguments: .testDefault) 244 | } 245 | } 246 | let response = Data(""" 247 | { 248 | "data": { 249 | "testObject": { 250 | "alias": 123 251 | } 252 | } 253 | } 254 | """.utf8) 255 | 256 | XCTAssertEqual(query.render(), "{testObject{alias:i3_intArgs\(testArgs)}}") 257 | let res: Partial? = try? query.createResult(from: response) 258 | XCTAssertEqual(res?.values.count, 1) 259 | let aliased = res?.i3_intArgs(alias: "alias") 260 | XCTAssertEqual(aliased, 123) 261 | } 262 | 263 | func testArrayAliasOnObject() throws { 264 | let query: _Operation> = .query { 265 | $0.testObject { 266 | $0.i3_ints(alias: "alias") 267 | } 268 | } 269 | let response = Data(""" 270 | { 271 | "data": { 272 | "testObject": { 273 | "alias": [123, 321] 274 | } 275 | } 276 | } 277 | """.utf8) 278 | 279 | XCTAssertEqual(query.render(), "{testObject{alias:i3_ints}}") 280 | let res: Partial? = try? query.createResult(from: response) 281 | XCTAssertEqual(res?.values.count, 1) 282 | let aliased = res?.i3_ints(alias: "alias") 283 | XCTAssertEqual(aliased?[safe: 0], 123) 284 | XCTAssertEqual(aliased?[safe: 1], 321) 285 | } 286 | 287 | } 288 | 289 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation 290 | 291 | extension TestInterface3_Int_TypeTests { 292 | func testSingleOnFragment() { 293 | let fragment = Fragment("fragName", on: Query.self) { 294 | $0.i3_int 295 | } 296 | let query: _Operation = .query { 297 | fragment 298 | } 299 | let response = Data(""" 300 | { 301 | "data": { 302 | "i3_int": 123 303 | } 304 | } 305 | """.utf8) 306 | 307 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i3_int}") 308 | let res = try? query.createResult(from: response) 309 | XCTAssertEqual(res, 123) 310 | } 311 | 312 | func testSingleArgsOnFragment() { 313 | let fragment = Fragment("fragName", on: Query.self) { 314 | $0.i3_intArgs(arguments: .testDefault) 315 | } 316 | let query: _Operation = .query { 317 | fragment 318 | } 319 | let response = Data(""" 320 | { 321 | "data": { 322 | "i3_intArgs": 123 323 | } 324 | } 325 | """.utf8) 326 | 327 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i3_intArgs\(testArgs)}") 328 | let res = try? query.createResult(from: response) 329 | XCTAssertEqual(res, 123) 330 | } 331 | 332 | func testArrayOnFragment() { 333 | let fragment = Fragment("fragName", on: Query.self) { 334 | $0.i3_ints 335 | } 336 | let query: _Operation = .query { 337 | fragment 338 | } 339 | let response = Data(""" 340 | { 341 | "data": { 342 | "i3_ints": [123, 321] 343 | } 344 | } 345 | """.utf8) 346 | 347 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i3_ints}") 348 | let res = try? query.createResult(from: response) 349 | XCTAssertEqual(res?.count, 2) 350 | XCTAssertEqual(res?[safe: 0], 123) 351 | XCTAssertEqual(res?[safe: 1], 321) 352 | } 353 | 354 | func testOptionalOnFragment() { 355 | let fragment = Fragment("fragName", on: Query.self) { 356 | $0.i3_intOptional 357 | } 358 | let query: _Operation = .query { 359 | fragment 360 | } 361 | let response = Data(""" 362 | { 363 | "data": { 364 | "i3_intOptional": 123 365 | } 366 | } 367 | """.utf8) 368 | 369 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i3_intOptional}") 370 | let res = try? query.createResult(from: response) 371 | XCTAssertEqual(res, 123) 372 | } 373 | } 374 | 375 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation with aliases 376 | 377 | extension TestInterface3_Int_TypeTests { 378 | func testSingleAliasOnFragment() { 379 | let fragment = Fragment("fragName", on: Query.self) { 380 | $0.i3_int(alias: "alias") 381 | } 382 | let query: _Operation = .query { 383 | fragment 384 | } 385 | let response = Data(""" 386 | { 387 | "data": { 388 | "alias": 123 389 | } 390 | } 391 | """.utf8) 392 | 393 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i3_int}") 394 | let res = try? query.createResult(from: response) 395 | XCTAssertEqual(res, 123) 396 | } 397 | 398 | func testSingleArgsAliasOnFragment() { 399 | let fragment = Fragment("fragName", on: Query.self) { 400 | $0.i3_intArgs(alias: "alias", arguments: .testDefault) 401 | } 402 | let query: _Operation = .query { 403 | fragment 404 | } 405 | let response = Data(""" 406 | { 407 | "data": { 408 | "alias": 123 409 | } 410 | } 411 | """.utf8) 412 | 413 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i3_intArgs\(testArgs)}") 414 | let res = try? query.createResult(from: response) 415 | XCTAssertEqual(res, 123) 416 | } 417 | } 418 | 419 | 420 | // MARK: - Tests to ensure fragments on TestInterface3 can be used on selections of Int or [Int] 421 | 422 | extension TestInterface3_Int_TypeTests { 423 | func testSingleOnTestInterface3Fragment() { 424 | let fragment = Fragment("fragName", on: TestInterface3.self) { 425 | $0.i3_int 426 | } 427 | let query: _Operation = .query { 428 | $0.testObject { 429 | fragment 430 | } 431 | } 432 | let response = Data(""" 433 | { 434 | "data": { 435 | "testObject": { 436 | "i3_int": 123 437 | } 438 | } 439 | } 440 | """.utf8) 441 | 442 | XCTAssertEqual(query.render(), "{testObject{...fragName}},fragment fragName on TestInterface3{i3_int}") 443 | let res: Partial? = try? query.createResult(from: response) 444 | XCTAssertEqual(res?.values.count, 1) 445 | XCTAssertEqual(res?.i3_int, 123) 446 | } 447 | 448 | func testArrayOnObjectFragment() { 449 | let fragment = Fragment("fragName", on: TestInterface3.self) { 450 | $0.i3_int 451 | } 452 | let query: _Operation = .query { 453 | $0.testObjects { 454 | fragment 455 | } 456 | } 457 | let response = Data(""" 458 | { 459 | "data": { 460 | "testObjects": [ 461 | { "i3_int": 123 }, 462 | { "i3_int": 321 } 463 | ] 464 | } 465 | } 466 | """.utf8) 467 | 468 | XCTAssertEqual(query.render(), "{testObjects{...fragName}},fragment fragName on TestInterface3{i3_int}") 469 | let res: [Partial]? = try? query.createResult(from: response) 470 | XCTAssertEqual(res?.count, 2) 471 | XCTAssertEqual(res?[safe: 0]?.i3_int, 123) 472 | XCTAssertEqual(res?[safe: 1]?.i3_int, 321) 473 | } 474 | 475 | func testOptionalOnObjectFragment() { 476 | let fragment = Fragment("fragName", on: TestInterface3.self) { 477 | $0.i3_int 478 | } 479 | let query: _Operation = .query { 480 | $0.testObjectOptional { 481 | fragment 482 | } 483 | } 484 | let response = Data(""" 485 | { 486 | "data": { 487 | "testObjectOptional": { 488 | "i3_int": 123 489 | } 490 | } 491 | } 492 | """.utf8) 493 | 494 | XCTAssertEqual(query.render(), "{testObjectOptional{...fragName}},fragment fragName on TestInterface3{i3_int}") 495 | let res: Partial? = try? query.createResult(from: response) 496 | XCTAssertEqual(res?.values.count, 1) 497 | XCTAssertEqual(res?.i3_int, 123) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TestInterface4/TestInterface4_Int_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | @testable import Artemis 6 | 7 | // MARK: - Tests to ensure single selections of Int and [Int] render as expected 8 | 9 | extension TestInterface4_Int_TypeTests { 10 | func testSingle() { 11 | let query: _Operation = .query { 12 | $0.i4_int 13 | } 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "i4_int": 123 18 | } 19 | } 20 | """.utf8) 21 | 22 | XCTAssertEqual(query.render(), "{i4_int}") 23 | let res = try? query.createResult(from: response) 24 | XCTAssertEqual(res, 123) 25 | } 26 | 27 | func testSingleArgs() { 28 | let query: _Operation = .query { 29 | $0.i4_intArgs(arguments: .testDefault) 30 | } 31 | let response = Data(""" 32 | { 33 | "data": { 34 | "i4_intArgs": 123 35 | } 36 | } 37 | """.utf8) 38 | 39 | XCTAssertEqual(query.render(), "{i4_intArgs\(testArgs)}") 40 | let res = try? query.createResult(from: response) 41 | XCTAssertEqual(res, 123) 42 | } 43 | 44 | func testArray() { 45 | let query: _Operation = .query { 46 | $0.i4_ints 47 | } 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "i4_ints": [123, 321] 52 | } 53 | } 54 | """.utf8) 55 | 56 | XCTAssertEqual(query.render(), "{i4_ints}") 57 | let res = try? query.createResult(from: response) 58 | XCTAssertEqual(res?.count, 2) 59 | XCTAssertEqual(res?[safe: 0], 123) 60 | XCTAssertEqual(res?[safe: 1], 321) 61 | } 62 | 63 | func testOptional() { 64 | let query: _Operation = .query { 65 | $0.i4_intOptional 66 | } 67 | let response = Data(""" 68 | { 69 | "data": { 70 | "i4_intOptional": 123 71 | } 72 | } 73 | """.utf8) 74 | 75 | XCTAssertEqual(query.render(), "{i4_intOptional}") 76 | let res = try? query.createResult(from: response) 77 | XCTAssertEqual(res, 123) 78 | } 79 | } 80 | 81 | // MARK: - Tests to ensure single selections of Int and [Int] with aliases render as expected 82 | 83 | extension TestInterface4_Int_TypeTests { 84 | func testSingleAlias() { 85 | let query: _Operation = .query { 86 | $0.i4_int(alias: "alias") 87 | } 88 | let response = Data(""" 89 | { 90 | "data": { 91 | "alias": 123 92 | } 93 | } 94 | """.utf8) 95 | 96 | XCTAssertEqual(query.render(), "{alias:i4_int}") 97 | let res = try? query.createResult(from: response) 98 | XCTAssertEqual(res, 123) 99 | } 100 | 101 | func testSingleArgsAlias() { 102 | let query: _Operation = .query { 103 | $0.i4_intArgs(alias: "alias", arguments: .testDefault) 104 | } 105 | let response = Data(""" 106 | { 107 | "data": { 108 | "alias": 123 109 | } 110 | } 111 | """.utf8) 112 | 113 | XCTAssertEqual(query.render(), "{alias:i4_intArgs\(testArgs)}") 114 | let res = try? query.createResult(from: response) 115 | XCTAssertEqual(res, 123) 116 | } 117 | } 118 | 119 | // MARK: - Tests to ensure selections render as expected on selections of Int and [Int] on an Object 120 | 121 | extension TestInterface4_Int_TypeTests { 122 | func testSingleOnObject() { 123 | let query: _Operation> = .query { 124 | $0.testObject { 125 | $0.i4_int 126 | } 127 | } 128 | let response = Data(""" 129 | { 130 | "data": { 131 | "testObject": { 132 | "i4_int": 123 133 | } 134 | } 135 | } 136 | """.utf8) 137 | 138 | XCTAssertEqual(query.render(), "{testObject{i4_int}}") 139 | let res: Partial? = try? query.createResult(from: response) 140 | XCTAssertEqual(res?.values.count, 1) 141 | XCTAssertEqual(res?.i4_int, 123) 142 | } 143 | 144 | func testSingleArgsOnObject() { 145 | let query: _Operation> = .query { 146 | $0.testObject { 147 | $0.i4_intArgs(arguments: .testDefault) 148 | } 149 | } 150 | let response = Data(""" 151 | { 152 | "data": { 153 | "testObject": { 154 | "i4_intArgs": 123 155 | } 156 | } 157 | } 158 | """.utf8) 159 | 160 | XCTAssertEqual(query.render(), "{testObject{i4_intArgs\(testArgs)}}") 161 | let res: Partial? = try? query.createResult(from: response) 162 | XCTAssertEqual(res?.values.count, 1) 163 | XCTAssertEqual(res?.i4_intArgs, 123) 164 | } 165 | 166 | func testArrayOnObject() { 167 | let query: _Operation> = .query { 168 | $0.testObject { 169 | $0.i4_ints 170 | } 171 | } 172 | let response = Data(""" 173 | { 174 | "data": { 175 | "testObject": { 176 | "i4_ints": [123, 321] 177 | } 178 | } 179 | } 180 | """.utf8) 181 | 182 | XCTAssertEqual(query.render(), "{testObject{i4_ints}}") 183 | let res: Partial? = try? query.createResult(from: response) 184 | XCTAssertEqual(res?.values.count, 1) 185 | XCTAssertEqual(res?.i4_ints?.count, 2) 186 | XCTAssertEqual(res?.i4_ints?[safe: 0], 123) 187 | XCTAssertEqual(res?.i4_ints?[safe: 1], 321) 188 | } 189 | 190 | func testOptionalOnObject() { 191 | let query: _Operation> = .query { 192 | $0.testObject { 193 | $0.i4_intOptional 194 | } 195 | } 196 | let response = Data(""" 197 | { 198 | "data": { 199 | "testObject": { 200 | "i4_intOptional": 123 201 | } 202 | } 203 | } 204 | """.utf8) 205 | 206 | XCTAssertEqual(query.render(), "{testObject{i4_intOptional}}") 207 | let res: Partial? = try? query.createResult(from: response) 208 | XCTAssertEqual(res?.values.count, 1) 209 | XCTAssertEqual(res?.i4_intOptional, 123) 210 | } 211 | 212 | } 213 | 214 | // MARK: - Tests to ensure an alias of Int and [Int] on an Object can be used to pull values out of a result 215 | 216 | extension TestInterface4_Int_TypeTests { 217 | func testSingleAliasOnObject() throws { 218 | let query: _Operation> = .query { 219 | $0.testObject { 220 | $0.i4_int(alias: "alias") 221 | } 222 | } 223 | let response = Data(""" 224 | { 225 | "data": { 226 | "testObject": { 227 | "alias": 123 228 | } 229 | } 230 | } 231 | """.utf8) 232 | 233 | XCTAssertEqual(query.render(), "{testObject{alias:i4_int}}") 234 | let res: Partial? = try? query.createResult(from: response) 235 | XCTAssertEqual(res?.values.count, 1) 236 | let aliased = res?.i4_int(alias: "alias") 237 | XCTAssertEqual(aliased, 123) 238 | } 239 | 240 | func testSingleArgsAliasOnObject() throws { 241 | let query: _Operation> = .query { 242 | $0.testObject { 243 | $0.i4_intArgs(alias: "alias", arguments: .testDefault) 244 | } 245 | } 246 | let response = Data(""" 247 | { 248 | "data": { 249 | "testObject": { 250 | "alias": 123 251 | } 252 | } 253 | } 254 | """.utf8) 255 | 256 | XCTAssertEqual(query.render(), "{testObject{alias:i4_intArgs\(testArgs)}}") 257 | let res: Partial? = try? query.createResult(from: response) 258 | XCTAssertEqual(res?.values.count, 1) 259 | let aliased = res?.i4_intArgs(alias: "alias") 260 | XCTAssertEqual(aliased, 123) 261 | } 262 | 263 | func testArrayAliasOnObject() throws { 264 | let query: _Operation> = .query { 265 | $0.testObject { 266 | $0.i4_ints(alias: "alias") 267 | } 268 | } 269 | let response = Data(""" 270 | { 271 | "data": { 272 | "testObject": { 273 | "alias": [123, 321] 274 | } 275 | } 276 | } 277 | """.utf8) 278 | 279 | XCTAssertEqual(query.render(), "{testObject{alias:i4_ints}}") 280 | let res: Partial? = try? query.createResult(from: response) 281 | XCTAssertEqual(res?.values.count, 1) 282 | let aliased = res?.i4_ints(alias: "alias") 283 | XCTAssertEqual(aliased?[safe: 0], 123) 284 | XCTAssertEqual(aliased?[safe: 1], 321) 285 | } 286 | 287 | } 288 | 289 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation 290 | 291 | extension TestInterface4_Int_TypeTests { 292 | func testSingleOnFragment() { 293 | let fragment = Fragment("fragName", on: Query.self) { 294 | $0.i4_int 295 | } 296 | let query: _Operation = .query { 297 | fragment 298 | } 299 | let response = Data(""" 300 | { 301 | "data": { 302 | "i4_int": 123 303 | } 304 | } 305 | """.utf8) 306 | 307 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i4_int}") 308 | let res = try? query.createResult(from: response) 309 | XCTAssertEqual(res, 123) 310 | } 311 | 312 | func testSingleArgsOnFragment() { 313 | let fragment = Fragment("fragName", on: Query.self) { 314 | $0.i4_intArgs(arguments: .testDefault) 315 | } 316 | let query: _Operation = .query { 317 | fragment 318 | } 319 | let response = Data(""" 320 | { 321 | "data": { 322 | "i4_intArgs": 123 323 | } 324 | } 325 | """.utf8) 326 | 327 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i4_intArgs\(testArgs)}") 328 | let res = try? query.createResult(from: response) 329 | XCTAssertEqual(res, 123) 330 | } 331 | 332 | func testArrayOnFragment() { 333 | let fragment = Fragment("fragName", on: Query.self) { 334 | $0.i4_ints 335 | } 336 | let query: _Operation = .query { 337 | fragment 338 | } 339 | let response = Data(""" 340 | { 341 | "data": { 342 | "i4_ints": [123, 321] 343 | } 344 | } 345 | """.utf8) 346 | 347 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i4_ints}") 348 | let res = try? query.createResult(from: response) 349 | XCTAssertEqual(res?.count, 2) 350 | XCTAssertEqual(res?[safe: 0], 123) 351 | XCTAssertEqual(res?[safe: 1], 321) 352 | } 353 | 354 | func testOptionalOnFragment() { 355 | let fragment = Fragment("fragName", on: Query.self) { 356 | $0.i4_intOptional 357 | } 358 | let query: _Operation = .query { 359 | fragment 360 | } 361 | let response = Data(""" 362 | { 363 | "data": { 364 | "i4_intOptional": 123 365 | } 366 | } 367 | """.utf8) 368 | 369 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i4_intOptional}") 370 | let res = try? query.createResult(from: response) 371 | XCTAssertEqual(res, 123) 372 | } 373 | } 374 | 375 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation with aliases 376 | 377 | extension TestInterface4_Int_TypeTests { 378 | func testSingleAliasOnFragment() { 379 | let fragment = Fragment("fragName", on: Query.self) { 380 | $0.i4_int(alias: "alias") 381 | } 382 | let query: _Operation = .query { 383 | fragment 384 | } 385 | let response = Data(""" 386 | { 387 | "data": { 388 | "alias": 123 389 | } 390 | } 391 | """.utf8) 392 | 393 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i4_int}") 394 | let res = try? query.createResult(from: response) 395 | XCTAssertEqual(res, 123) 396 | } 397 | 398 | func testSingleArgsAliasOnFragment() { 399 | let fragment = Fragment("fragName", on: Query.self) { 400 | $0.i4_intArgs(alias: "alias", arguments: .testDefault) 401 | } 402 | let query: _Operation = .query { 403 | fragment 404 | } 405 | let response = Data(""" 406 | { 407 | "data": { 408 | "alias": 123 409 | } 410 | } 411 | """.utf8) 412 | 413 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i4_intArgs\(testArgs)}") 414 | let res = try? query.createResult(from: response) 415 | XCTAssertEqual(res, 123) 416 | } 417 | } 418 | 419 | 420 | // MARK: - Tests to ensure fragments on TestInterface4 can be used on selections of Int or [Int] 421 | 422 | extension TestInterface4_Int_TypeTests { 423 | func testSingleOnTestInterface4Fragment() { 424 | let fragment = Fragment("fragName", on: TestInterface4.self) { 425 | $0.i4_int 426 | } 427 | let query: _Operation = .query { 428 | $0.testObject { 429 | fragment 430 | } 431 | } 432 | let response = Data(""" 433 | { 434 | "data": { 435 | "testObject": { 436 | "i4_int": 123 437 | } 438 | } 439 | } 440 | """.utf8) 441 | 442 | XCTAssertEqual(query.render(), "{testObject{...fragName}},fragment fragName on TestInterface4{i4_int}") 443 | let res: Partial? = try? query.createResult(from: response) 444 | XCTAssertEqual(res?.values.count, 1) 445 | XCTAssertEqual(res?.i4_int, 123) 446 | } 447 | 448 | func testArrayOnObjectFragment() { 449 | let fragment = Fragment("fragName", on: TestInterface4.self) { 450 | $0.i4_int 451 | } 452 | let query: _Operation = .query { 453 | $0.testObjects { 454 | fragment 455 | } 456 | } 457 | let response = Data(""" 458 | { 459 | "data": { 460 | "testObjects": [ 461 | { "i4_int": 123 }, 462 | { "i4_int": 321 } 463 | ] 464 | } 465 | } 466 | """.utf8) 467 | 468 | XCTAssertEqual(query.render(), "{testObjects{...fragName}},fragment fragName on TestInterface4{i4_int}") 469 | let res: [Partial]? = try? query.createResult(from: response) 470 | XCTAssertEqual(res?.count, 2) 471 | XCTAssertEqual(res?[safe: 0]?.i4_int, 123) 472 | XCTAssertEqual(res?[safe: 1]?.i4_int, 321) 473 | } 474 | 475 | func testOptionalOnObjectFragment() { 476 | let fragment = Fragment("fragName", on: TestInterface4.self) { 477 | $0.i4_int 478 | } 479 | let query: _Operation = .query { 480 | $0.testObjectOptional { 481 | fragment 482 | } 483 | } 484 | let response = Data(""" 485 | { 486 | "data": { 487 | "testObjectOptional": { 488 | "i4_int": 123 489 | } 490 | } 491 | } 492 | """.utf8) 493 | 494 | XCTAssertEqual(query.render(), "{testObjectOptional{...fragName}},fragment fragName on TestInterface4{i4_int}") 495 | let res: Partial? = try? query.createResult(from: response) 496 | XCTAssertEqual(res?.values.count, 1) 497 | XCTAssertEqual(res?.i4_int, 123) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Tests/ArtemisTests/TestInterface5/TestInterface5_Int_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import XCTest 5 | @testable import Artemis 6 | 7 | // MARK: - Tests to ensure single selections of Int and [Int] render as expected 8 | 9 | extension TestInterface5_Int_TypeTests { 10 | func testSingle() { 11 | let query: _Operation = .query { 12 | $0.i5_int 13 | } 14 | let response = Data(""" 15 | { 16 | "data": { 17 | "i5_int": 123 18 | } 19 | } 20 | """.utf8) 21 | 22 | XCTAssertEqual(query.render(), "{i5_int}") 23 | let res = try? query.createResult(from: response) 24 | XCTAssertEqual(res, 123) 25 | } 26 | 27 | func testSingleArgs() { 28 | let query: _Operation = .query { 29 | $0.i5_intArgs(arguments: .testDefault) 30 | } 31 | let response = Data(""" 32 | { 33 | "data": { 34 | "i5_intArgs": 123 35 | } 36 | } 37 | """.utf8) 38 | 39 | XCTAssertEqual(query.render(), "{i5_intArgs\(testArgs)}") 40 | let res = try? query.createResult(from: response) 41 | XCTAssertEqual(res, 123) 42 | } 43 | 44 | func testArray() { 45 | let query: _Operation = .query { 46 | $0.i5_ints 47 | } 48 | let response = Data(""" 49 | { 50 | "data": { 51 | "i5_ints": [123, 321] 52 | } 53 | } 54 | """.utf8) 55 | 56 | XCTAssertEqual(query.render(), "{i5_ints}") 57 | let res = try? query.createResult(from: response) 58 | XCTAssertEqual(res?.count, 2) 59 | XCTAssertEqual(res?[safe: 0], 123) 60 | XCTAssertEqual(res?[safe: 1], 321) 61 | } 62 | 63 | func testOptional() { 64 | let query: _Operation = .query { 65 | $0.i5_intOptional 66 | } 67 | let response = Data(""" 68 | { 69 | "data": { 70 | "i5_intOptional": 123 71 | } 72 | } 73 | """.utf8) 74 | 75 | XCTAssertEqual(query.render(), "{i5_intOptional}") 76 | let res = try? query.createResult(from: response) 77 | XCTAssertEqual(res, 123) 78 | } 79 | } 80 | 81 | // MARK: - Tests to ensure single selections of Int and [Int] with aliases render as expected 82 | 83 | extension TestInterface5_Int_TypeTests { 84 | func testSingleAlias() { 85 | let query: _Operation = .query { 86 | $0.i5_int(alias: "alias") 87 | } 88 | let response = Data(""" 89 | { 90 | "data": { 91 | "alias": 123 92 | } 93 | } 94 | """.utf8) 95 | 96 | XCTAssertEqual(query.render(), "{alias:i5_int}") 97 | let res = try? query.createResult(from: response) 98 | XCTAssertEqual(res, 123) 99 | } 100 | 101 | func testSingleArgsAlias() { 102 | let query: _Operation = .query { 103 | $0.i5_intArgs(alias: "alias", arguments: .testDefault) 104 | } 105 | let response = Data(""" 106 | { 107 | "data": { 108 | "alias": 123 109 | } 110 | } 111 | """.utf8) 112 | 113 | XCTAssertEqual(query.render(), "{alias:i5_intArgs\(testArgs)}") 114 | let res = try? query.createResult(from: response) 115 | XCTAssertEqual(res, 123) 116 | } 117 | } 118 | 119 | // MARK: - Tests to ensure selections render as expected on selections of Int and [Int] on an Object 120 | 121 | extension TestInterface5_Int_TypeTests { 122 | func testSingleOnObject() { 123 | let query: _Operation> = .query { 124 | $0.testObject { 125 | $0.i5_int 126 | } 127 | } 128 | let response = Data(""" 129 | { 130 | "data": { 131 | "testObject": { 132 | "i5_int": 123 133 | } 134 | } 135 | } 136 | """.utf8) 137 | 138 | XCTAssertEqual(query.render(), "{testObject{i5_int}}") 139 | let res: Partial? = try? query.createResult(from: response) 140 | XCTAssertEqual(res?.values.count, 1) 141 | XCTAssertEqual(res?.i5_int, 123) 142 | } 143 | 144 | func testSingleArgsOnObject() { 145 | let query: _Operation> = .query { 146 | $0.testObject { 147 | $0.i5_intArgs(arguments: .testDefault) 148 | } 149 | } 150 | let response = Data(""" 151 | { 152 | "data": { 153 | "testObject": { 154 | "i5_intArgs": 123 155 | } 156 | } 157 | } 158 | """.utf8) 159 | 160 | XCTAssertEqual(query.render(), "{testObject{i5_intArgs\(testArgs)}}") 161 | let res: Partial? = try? query.createResult(from: response) 162 | XCTAssertEqual(res?.values.count, 1) 163 | XCTAssertEqual(res?.i5_intArgs, 123) 164 | } 165 | 166 | func testArrayOnObject() { 167 | let query: _Operation> = .query { 168 | $0.testObject { 169 | $0.i5_ints 170 | } 171 | } 172 | let response = Data(""" 173 | { 174 | "data": { 175 | "testObject": { 176 | "i5_ints": [123, 321] 177 | } 178 | } 179 | } 180 | """.utf8) 181 | 182 | XCTAssertEqual(query.render(), "{testObject{i5_ints}}") 183 | let res: Partial? = try? query.createResult(from: response) 184 | XCTAssertEqual(res?.values.count, 1) 185 | XCTAssertEqual(res?.i5_ints?.count, 2) 186 | XCTAssertEqual(res?.i5_ints?[safe: 0], 123) 187 | XCTAssertEqual(res?.i5_ints?[safe: 1], 321) 188 | } 189 | 190 | func testOptionalOnObject() { 191 | let query: _Operation> = .query { 192 | $0.testObject { 193 | $0.i5_intOptional 194 | } 195 | } 196 | let response = Data(""" 197 | { 198 | "data": { 199 | "testObject": { 200 | "i5_intOptional": 123 201 | } 202 | } 203 | } 204 | """.utf8) 205 | 206 | XCTAssertEqual(query.render(), "{testObject{i5_intOptional}}") 207 | let res: Partial? = try? query.createResult(from: response) 208 | XCTAssertEqual(res?.values.count, 1) 209 | XCTAssertEqual(res?.i5_intOptional, 123) 210 | } 211 | 212 | } 213 | 214 | // MARK: - Tests to ensure an alias of Int and [Int] on an Object can be used to pull values out of a result 215 | 216 | extension TestInterface5_Int_TypeTests { 217 | func testSingleAliasOnObject() throws { 218 | let query: _Operation> = .query { 219 | $0.testObject { 220 | $0.i5_int(alias: "alias") 221 | } 222 | } 223 | let response = Data(""" 224 | { 225 | "data": { 226 | "testObject": { 227 | "alias": 123 228 | } 229 | } 230 | } 231 | """.utf8) 232 | 233 | XCTAssertEqual(query.render(), "{testObject{alias:i5_int}}") 234 | let res: Partial? = try? query.createResult(from: response) 235 | XCTAssertEqual(res?.values.count, 1) 236 | let aliased = res?.i5_int(alias: "alias") 237 | XCTAssertEqual(aliased, 123) 238 | } 239 | 240 | func testSingleArgsAliasOnObject() throws { 241 | let query: _Operation> = .query { 242 | $0.testObject { 243 | $0.i5_intArgs(alias: "alias", arguments: .testDefault) 244 | } 245 | } 246 | let response = Data(""" 247 | { 248 | "data": { 249 | "testObject": { 250 | "alias": 123 251 | } 252 | } 253 | } 254 | """.utf8) 255 | 256 | XCTAssertEqual(query.render(), "{testObject{alias:i5_intArgs\(testArgs)}}") 257 | let res: Partial? = try? query.createResult(from: response) 258 | XCTAssertEqual(res?.values.count, 1) 259 | let aliased = res?.i5_intArgs(alias: "alias") 260 | XCTAssertEqual(aliased, 123) 261 | } 262 | 263 | func testArrayAliasOnObject() throws { 264 | let query: _Operation> = .query { 265 | $0.testObject { 266 | $0.i5_ints(alias: "alias") 267 | } 268 | } 269 | let response = Data(""" 270 | { 271 | "data": { 272 | "testObject": { 273 | "alias": [123, 321] 274 | } 275 | } 276 | } 277 | """.utf8) 278 | 279 | XCTAssertEqual(query.render(), "{testObject{alias:i5_ints}}") 280 | let res: Partial? = try? query.createResult(from: response) 281 | XCTAssertEqual(res?.values.count, 1) 282 | let aliased = res?.i5_ints(alias: "alias") 283 | XCTAssertEqual(aliased?[safe: 0], 123) 284 | XCTAssertEqual(aliased?[safe: 1], 321) 285 | } 286 | 287 | } 288 | 289 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation 290 | 291 | extension TestInterface5_Int_TypeTests { 292 | func testSingleOnFragment() { 293 | let fragment = Fragment("fragName", on: Query.self) { 294 | $0.i5_int 295 | } 296 | let query: _Operation = .query { 297 | fragment 298 | } 299 | let response = Data(""" 300 | { 301 | "data": { 302 | "i5_int": 123 303 | } 304 | } 305 | """.utf8) 306 | 307 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i5_int}") 308 | let res = try? query.createResult(from: response) 309 | XCTAssertEqual(res, 123) 310 | } 311 | 312 | func testSingleArgsOnFragment() { 313 | let fragment = Fragment("fragName", on: Query.self) { 314 | $0.i5_intArgs(arguments: .testDefault) 315 | } 316 | let query: _Operation = .query { 317 | fragment 318 | } 319 | let response = Data(""" 320 | { 321 | "data": { 322 | "i5_intArgs": 123 323 | } 324 | } 325 | """.utf8) 326 | 327 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i5_intArgs\(testArgs)}") 328 | let res = try? query.createResult(from: response) 329 | XCTAssertEqual(res, 123) 330 | } 331 | 332 | func testArrayOnFragment() { 333 | let fragment = Fragment("fragName", on: Query.self) { 334 | $0.i5_ints 335 | } 336 | let query: _Operation = .query { 337 | fragment 338 | } 339 | let response = Data(""" 340 | { 341 | "data": { 342 | "i5_ints": [123, 321] 343 | } 344 | } 345 | """.utf8) 346 | 347 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i5_ints}") 348 | let res = try? query.createResult(from: response) 349 | XCTAssertEqual(res?.count, 2) 350 | XCTAssertEqual(res?[safe: 0], 123) 351 | XCTAssertEqual(res?[safe: 1], 321) 352 | } 353 | 354 | func testOptionalOnFragment() { 355 | let fragment = Fragment("fragName", on: Query.self) { 356 | $0.i5_intOptional 357 | } 358 | let query: _Operation = .query { 359 | fragment 360 | } 361 | let response = Data(""" 362 | { 363 | "data": { 364 | "i5_intOptional": 123 365 | } 366 | } 367 | """.utf8) 368 | 369 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{i5_intOptional}") 370 | let res = try? query.createResult(from: response) 371 | XCTAssertEqual(res, 123) 372 | } 373 | } 374 | 375 | // MARK: - Tests to ensure fragments on Query selecting Int and [Int] can be used at the top level of an operation with aliases 376 | 377 | extension TestInterface5_Int_TypeTests { 378 | func testSingleAliasOnFragment() { 379 | let fragment = Fragment("fragName", on: Query.self) { 380 | $0.i5_int(alias: "alias") 381 | } 382 | let query: _Operation = .query { 383 | fragment 384 | } 385 | let response = Data(""" 386 | { 387 | "data": { 388 | "alias": 123 389 | } 390 | } 391 | """.utf8) 392 | 393 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i5_int}") 394 | let res = try? query.createResult(from: response) 395 | XCTAssertEqual(res, 123) 396 | } 397 | 398 | func testSingleArgsAliasOnFragment() { 399 | let fragment = Fragment("fragName", on: Query.self) { 400 | $0.i5_intArgs(alias: "alias", arguments: .testDefault) 401 | } 402 | let query: _Operation = .query { 403 | fragment 404 | } 405 | let response = Data(""" 406 | { 407 | "data": { 408 | "alias": 123 409 | } 410 | } 411 | """.utf8) 412 | 413 | XCTAssertEqual(query.render(), "{...fragName},fragment fragName on Query{alias:i5_intArgs\(testArgs)}") 414 | let res = try? query.createResult(from: response) 415 | XCTAssertEqual(res, 123) 416 | } 417 | } 418 | 419 | 420 | // MARK: - Tests to ensure fragments on TestInterface5 can be used on selections of Int or [Int] 421 | 422 | extension TestInterface5_Int_TypeTests { 423 | func testSingleOnTestInterface5Fragment() { 424 | let fragment = Fragment("fragName", on: TestInterface5.self) { 425 | $0.i5_int 426 | } 427 | let query: _Operation = .query { 428 | $0.testObject { 429 | fragment 430 | } 431 | } 432 | let response = Data(""" 433 | { 434 | "data": { 435 | "testObject": { 436 | "i5_int": 123 437 | } 438 | } 439 | } 440 | """.utf8) 441 | 442 | XCTAssertEqual(query.render(), "{testObject{...fragName}},fragment fragName on TestInterface5{i5_int}") 443 | let res: Partial? = try? query.createResult(from: response) 444 | XCTAssertEqual(res?.values.count, 1) 445 | XCTAssertEqual(res?.i5_int, 123) 446 | } 447 | 448 | func testArrayOnObjectFragment() { 449 | let fragment = Fragment("fragName", on: TestInterface5.self) { 450 | $0.i5_int 451 | } 452 | let query: _Operation = .query { 453 | $0.testObjects { 454 | fragment 455 | } 456 | } 457 | let response = Data(""" 458 | { 459 | "data": { 460 | "testObjects": [ 461 | { "i5_int": 123 }, 462 | { "i5_int": 321 } 463 | ] 464 | } 465 | } 466 | """.utf8) 467 | 468 | XCTAssertEqual(query.render(), "{testObjects{...fragName}},fragment fragName on TestInterface5{i5_int}") 469 | let res: [Partial]? = try? query.createResult(from: response) 470 | XCTAssertEqual(res?.count, 2) 471 | XCTAssertEqual(res?[safe: 0]?.i5_int, 123) 472 | XCTAssertEqual(res?[safe: 1]?.i5_int, 321) 473 | } 474 | 475 | func testOptionalOnObjectFragment() { 476 | let fragment = Fragment("fragName", on: TestInterface5.self) { 477 | $0.i5_int 478 | } 479 | let query: _Operation = .query { 480 | $0.testObjectOptional { 481 | fragment 482 | } 483 | } 484 | let response = Data(""" 485 | { 486 | "data": { 487 | "testObjectOptional": { 488 | "i5_int": 123 489 | } 490 | } 491 | } 492 | """.utf8) 493 | 494 | XCTAssertEqual(query.render(), "{testObjectOptional{...fragName}},fragment fragName on TestInterface5{i5_int}") 495 | let res: Partial? = try? query.createResult(from: response) 496 | XCTAssertEqual(res?.values.count, 1) 497 | XCTAssertEqual(res?.i5_int, 123) 498 | } 499 | } 500 | --------------------------------------------------------------------------------