From 876c29b4b0a7ebc3ebf146caea1e2611d69d3790 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Tue, 21 Dec 2021 17:09:54 +0000 Subject: [PATCH 01/19] Rework AST to be more type safe and allow mutation Also replace existential types with enums --- Sources/GraphQL/Execution/Execute.swift | 34 +- Sources/GraphQL/Language/AST.swift | 1367 +++++++++------ Sources/GraphQL/Language/Parser.swift | 131 +- Sources/GraphQL/Language/Visitor.swift | 1047 ++++++++---- Sources/GraphQL/Type/Definition.swift | 161 +- Sources/GraphQL/Type/Directives.swift | 2 +- Sources/GraphQL/Type/Introspection.swift | 4 + Sources/GraphQL/Type/Scalars.swift | 18 +- Sources/GraphQL/Utilities/ASTFromValue.swift | 16 +- .../GraphQL/Utilities/BuildClientSchema.swift | 162 ++ .../Utilities/GetIntrospectionQuery.swift | 382 +++++ Sources/GraphQL/Utilities/TypeFromAST.swift | 16 +- Sources/GraphQL/Utilities/TypeInfo.swift | 24 +- Sources/GraphQL/Utilities/ValueFromAST.swift | 6 +- .../Rules/FieldsOnCorrectTypeRule.swift | 77 +- .../Rules/KnownArgumentNamesRule.swift | 38 +- .../Rules/NoUnusedVariablesRule.swift | 76 +- .../Rules/PossibleFragmentSpreadsRule.swift | 117 +- .../Rules/ProvidedNonNullArgumentsRule.swift | 53 +- .../Validation/Rules/ScalarLeafsRule.swift | 42 +- .../GraphQL/Validation/SpecifiedRules.swift | 50 +- Sources/GraphQL/Validation/Validate.swift | 243 ++- .../LanguageTests/ParserTests.swift | 916 +++++----- .../LanguageTests/SchemaParserTests.swift | 1472 ++++++++--------- .../LanguageTests/VisitorTests.swift | 120 +- .../TypeTests/DecodableTests.swift | 32 + .../FieldsOnCorrectTypeTests.swift | 2 +- .../KnownArgumentNamesTests.swift | 2 +- .../NoUnusedVariablesRuleTests.swift | 2 +- ...PossibleFragmentSpreadsRuleRuleTests.swift | 2 +- .../ProvidedNonNullArgumentsTests.swift | 2 +- .../ValidationTests/ValidationTests.swift | 4 +- 32 files changed, 4047 insertions(+), 2573 deletions(-) create mode 100644 Sources/GraphQL/Utilities/BuildClientSchema.swift create mode 100644 Sources/GraphQL/Utilities/GetIntrospectionQuery.swift create mode 100644 Tests/GraphQLTests/TypeTests/DecodableTests.swift diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 486eb994..0641bed2 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -359,23 +359,21 @@ func buildExecutionContext( for definition in documentAST.definitions { switch definition { - case let definition as OperationDefinition: + case .executableDefinition(.operation(let definition)): guard !(operationName == nil && possibleOperation != nil) else { throw GraphQLError( message: "Must provide operation name if query contains multiple operations." ) } - + if operationName == nil || definition.name?.value == operationName { possibleOperation = definition } - - case let definition as FragmentDefinition: + case .executableDefinition(.fragment(let definition)): fragments[definition.name.value] = definition - default: throw GraphQLError( - message: "GraphQL cannot execute a request containing a \(definition.kind).", + message: "GraphQL cannot execute a request containing a \(definition.underlyingNode.kind).", nodes: [definition] ) } @@ -502,7 +500,7 @@ func collectFields( for selection in selectionSet.selections { switch selection { - case let field as Field: + case .field(let field): let shouldInclude = try shouldIncludeNode( exeContext: exeContext, directives: field.directives @@ -519,7 +517,7 @@ func collectFields( } fields[name]?.append(field) - case let inlineFragment as InlineFragment: + case .inlineFragment(let inlineFragment): let shouldInclude = try shouldIncludeNode( exeContext: exeContext, directives: inlineFragment.directives @@ -542,34 +540,34 @@ func collectFields( fields: &fields, visitedFragmentNames: &visitedFragmentNames ) - case let fragmentSpread as FragmentSpread: + case .fragmentSpread(let fragmentSpread): let fragmentName = fragmentSpread.name.value - + let shouldInclude = try shouldIncludeNode( exeContext: exeContext, directives: fragmentSpread.directives ) - + guard visitedFragmentNames[fragmentName] == nil && shouldInclude else { continue } - + visitedFragmentNames[fragmentName] = true - + guard let fragment = exeContext.fragments[fragmentName] else { continue } - + let fragmentConditionMatches = try doesFragmentConditionMatch( exeContext: exeContext, fragment: fragment, type: runtimeType ) - + guard fragmentConditionMatches else { continue } - + try collectFields( exeContext: exeContext, runtimeType: runtimeType, @@ -577,8 +575,6 @@ func collectFields( fields: &fields, visitedFragmentNames: &visitedFragmentNames ) - default: - break } } @@ -629,7 +625,7 @@ func doesFragmentConditionMatch( return true } - guard let conditionalType = typeFromAST(schema: exeContext.schema, inputTypeAST: typeConditionAST) else { + guard let conditionalType = typeFromAST(schema: exeContext.schema, inputTypeAST: .namedType(typeConditionAST)) else { return true } diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 51ba839f..fcb70388 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -133,42 +133,30 @@ extension Token : CustomStringConvertible { } } -public enum NodeResult { - case node(Node) - case array([Node]) - - public var isNode: Bool { - if case .node = self { - return true - } - return false - } - - public var isArray: Bool { - if case .array = self { - return true - } - return false - } -} - /** * The list of all possible AST node types. */ -public protocol Node { - var kind: Kind { get } +public protocol Node: TextOutputStreamable { var loc: Location? { get } - func get(key: String) -> NodeResult? - func set(value: Node?, key: String) + mutating func descend(descender: inout Descender) } extension Node { - public func get(key: String) -> NodeResult? { - return nil + var printed: String { + var s = "" + self.write(to: &s) + return s } +} - public func set(value: Node?, key: String) { - +private protocol EnumNode: Node { + var underlyingNode: Node { get } +} +extension EnumNode { + public var loc: Location? { underlyingNode.loc } + + public func write(to target: inout Target) where Target : TextOutputStream { + underlyingNode.write(to: &target) } } @@ -178,15 +166,18 @@ extension OperationDefinition : Node {} extension VariableDefinition : Node {} extension Variable : Node {} extension SelectionSet : Node {} +extension Selection : Node {} extension Field : Node {} extension Argument : Node {} extension FragmentSpread : Node {} extension InlineFragment : Node {} extension FragmentDefinition : Node {} +extension Value : Node {} extension IntValue : Node {} extension FloatValue : Node {} extension StringValue : Node {} extension BooleanValue : Node {} +extension NullValue : Node {} extension EnumValue : Node {} extension ListValue : Node {} extension ObjectValue : Node {} @@ -209,42 +200,50 @@ extension InputObjectTypeDefinition : Node {} extension TypeExtensionDefinition : Node {} extension DirectiveDefinition : Node {} -public final class Name { +public struct Name { public let kind: Kind = .name public let loc: Location? public let value: String - init(loc: Location? = nil, value: String) { + public init(loc: Location? = nil, value: String) { self.loc = loc self.value = value } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write(value) + } } -extension Name : Equatable { +extension Name: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } public static func == (lhs: Name, rhs: Name) -> Bool { return lhs.value == rhs.value } } -public final class Document { +public struct Document { public let kind: Kind = .document public let loc: Location? - public let definitions: [Definition] + public var definitions: [Definition] init(loc: Location? = nil, definitions: [Definition]) { self.loc = loc self.definitions = definitions } - public func get(key: String) -> NodeResult? { - switch key { - case "definitions": - guard !definitions.isEmpty else { - return nil - } - return .array(definitions) - default: - return nil + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.definitions) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + definitions.forEach { + $0.write(to: &target) + target.write("\n\n") } } } @@ -265,29 +264,73 @@ extension Document : Equatable { } } -public protocol Definition : Node {} -extension OperationDefinition : Definition {} -extension FragmentDefinition : Definition {} +public enum Definition: EnumNode, Equatable { + case executableDefinition(ExecutableDefinition) + case typeSystemDefinitionOrExtension(TypeSystemDefinitionOrExtension) + + var underlyingNode: Node { + switch self { + case let .executableDefinition(x): + return x + case let .typeSystemDefinitionOrExtension(x): + return x + } + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .executableDefinition(x): + descender.descend(enumCase: &x) + self = .executableDefinition(x) + case var .typeSystemDefinitionOrExtension(x): + descender.descend(enumCase: &x) + self = .typeSystemDefinitionOrExtension(x) + } + } +} + +public enum ExecutableDefinition: EnumNode, Equatable { + case operation(OperationDefinition) + case fragment(FragmentDefinition) -public func == (lhs: Definition, rhs: Definition) -> Bool { - switch lhs { - case let l as OperationDefinition: - if let r = rhs as? OperationDefinition { - return l == r + fileprivate var underlyingNode: Node { + switch self { + case let .fragment(fragmentDef): + return fragmentDef + case let .operation(operationDef): + return operationDef } - case let l as FragmentDefinition: - if let r = rhs as? FragmentDefinition { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .fragment(x): + descender.descend(enumCase: &x) + self = .fragment(x) + case var .operation(x): + descender.descend(enumCase: &x) + self = .operation(x) } - case let l as TypeSystemDefinition: - if let r = rhs as? TypeSystemDefinition { - return l == r + } +} + +public enum TypeSystemDefinitionOrExtension: EnumNode, Equatable { + case typeSystemDefinition(TypeSystemDefinition) + + fileprivate var underlyingNode: Node { + switch self { + case let .typeSystemDefinition(x): + return x } - default: - return false } - return false + public mutating func descend(descender: inout Descender) { + switch self { + case var .typeSystemDefinition(x): + descender.descend(enumCase: &x) + self = .typeSystemDefinition(x) + } + } } public enum OperationType : String { @@ -297,14 +340,14 @@ public enum OperationType : String { case subscription = "subscription" } -public final class OperationDefinition { +public struct OperationDefinition { public let kind: Kind = .operationDefinition public let loc: Location? - public let operation: OperationType - public let name: Name? - public let variableDefinitions: [VariableDefinition] - public let directives: [Directive] - public let selectionSet: SelectionSet + public var operation: OperationType + public var name: Name? + public var variableDefinitions: [VariableDefinition] + public var directives: [Directive] + public var selectionSet: SelectionSet init(loc: Location? = nil, operation: OperationType, name: Name? = nil, variableDefinitions: [VariableDefinition] = [], directives: [Directive] = [], selectionSet: SelectionSet) { self.loc = loc @@ -315,48 +358,53 @@ public final class OperationDefinition { self.selectionSet = selectionSet } - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return name.map({ .node($0) }) - case "variableDefinitions": - guard !variableDefinitions.isEmpty else { - return nil + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.variableDefinitions) + descender.descend(&self, \.directives) + descender.descend(&self, \.selectionSet) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + let anonymous = operation == .query && directives.isEmpty && variableDefinitions.isEmpty + if !anonymous { + target.write(operation.rawValue) + target.write(" ") + name?.write(to: &target) + if let first = variableDefinitions.first { + target.write(" (") + first.write(to: &target) + variableDefinitions.suffix(from: 1).forEach { + target.write(", ") + $0.write(to: &target) + } + target.write(")") } - return .array(variableDefinitions) - case "directives": - guard !variableDefinitions.isEmpty else { - return nil + if !directives.isEmpty { + directives.write(to: &target) } - return .array(directives) - case "selectionSet": - return .node(selectionSet) - default: - return nil } + target.write(" ") + selectionSet.write(to: &target) } } -extension OperationDefinition : Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - +extension OperationDefinition: Equatable { public static func == (lhs: OperationDefinition, rhs: OperationDefinition) -> Bool { return lhs.operation == rhs.operation && - lhs.name == rhs.name && - lhs.variableDefinitions == rhs.variableDefinitions && - lhs.directives == rhs.directives && - lhs.selectionSet == rhs.selectionSet + lhs.name == rhs.name && + lhs.variableDefinitions == rhs.variableDefinitions && + lhs.directives == rhs.directives && + lhs.selectionSet == rhs.selectionSet } } -public final class VariableDefinition { +public struct VariableDefinition { public let kind: Kind = .variableDefinition public let loc: Location? - public let variable: Variable - public let type: Type - public let defaultValue: Value? + public var variable: Variable + public var type: Type + public var defaultValue: Value? init(loc: Location? = nil, variable: Variable, type: Type, defaultValue: Value? = nil) { self.loc = loc @@ -364,17 +412,20 @@ public final class VariableDefinition { self.type = type self.defaultValue = defaultValue } - - public func get(key: String) -> NodeResult? { - switch key { - case "variable": - return .node(variable) - case "type": - return .node(type) - case "defaultValue": - return defaultValue.map({ .node($0) }) - default: - return nil + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.variable) + descender.descend(&self, \.type) + descender.descend(&self, \.defaultValue) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + variable.write(to: &target) + target.write(": ") + type.write(to: &target) + if let defaultValue = defaultValue { + target.write(" = ") + defaultValue.write(to: &target) } } } @@ -401,23 +452,23 @@ extension VariableDefinition : Equatable { } } -public final class Variable { +public struct Variable { public let kind: Kind = .variable public let loc: Location? - public let name: Name + public var name: Name init(loc: Location? = nil, name: Name) { self.loc = loc self.name = name } - - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return .node(name) - default: - return nil - } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("$") + name.write(to: &target) } } @@ -427,85 +478,87 @@ extension Variable : Equatable { } } -public final class SelectionSet { +public struct SelectionSet { public let kind: Kind = .selectionSet public let loc: Location? - public let selections: [Selection] + public var selections: [Selection] - init(loc: Location? = nil, selections: [Selection]) { + public init(loc: Location? = nil, selections: [Selection]) { self.loc = loc self.selections = selections } - - public func get(key: String) -> NodeResult? { - switch key { - case "selections": - guard !selections.isEmpty else { - return nil - } - return .array(selections) - default: - return nil + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.selections) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("{\n") + selections.forEach { + $0.write(to: &target) + target.write("\n") } + target.write("}") } } -extension SelectionSet : Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - +extension SelectionSet: Equatable { public static func == (lhs: SelectionSet, rhs: SelectionSet) -> Bool { guard lhs.selections.count == rhs.selections.count else { return false } - + for (l, r) in zip(lhs.selections, rhs.selections) { guard l == r else { return false } } - + return true } } -public protocol Selection : Node {} -extension Field : Selection {} -extension FragmentSpread : Selection {} -extension InlineFragment : Selection {} - -public func == (lhs: Selection, rhs: Selection) -> Bool { - switch lhs { - case let l as Field: - if let r = rhs as? Field { - return l == r - } - case let l as FragmentSpread: - if let r = rhs as? FragmentSpread { - return l == r +public enum Selection: EnumNode, Equatable { + case field(Field) + case fragmentSpread(FragmentSpread) + case inlineFragment(InlineFragment) + + fileprivate var underlyingNode: Node { + switch self { + case let .field(field): + return field + case let .fragmentSpread(fragmentSpread): + return fragmentSpread + case let .inlineFragment(inlineFragment): + return inlineFragment } - case let l as InlineFragment: - if let r = rhs as? InlineFragment { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .field(x): + descender.descend(enumCase: &x) + self = .field(x) + case var .fragmentSpread(x): + descender.descend(enumCase: &x) + self = .fragmentSpread(x) + case var .inlineFragment(x): + descender.descend(enumCase: &x) + self = .inlineFragment(x) } - default: - return false } - - return false } -public final class Field { +public struct Field { public let kind: Kind = .field public let loc: Location? - public let alias: Name? - public let name: Name - public let arguments: [Argument] - public let directives: [Directive] - public let selectionSet: SelectionSet? + public var alias: Name? + public var name: Name + public var arguments: [Argument] + public var directives: [Directive] + public var selectionSet: SelectionSet? - init(loc: Location? = nil, alias: Name? = nil, name: Name, arguments: [Argument] = [], directives: [Directive] = [], selectionSet: SelectionSet? = nil) { + public init(loc: Location? = nil, alias: Name? = nil, name: Name, arguments: [Argument] = [], directives: [Directive] = [], selectionSet: SelectionSet? = nil) { self.loc = loc self.alias = alias self.name = name @@ -513,27 +566,33 @@ public final class Field { self.directives = directives self.selectionSet = selectionSet } - - public func get(key: String) -> NodeResult? { - switch key { - case "alias": - return alias.map({ .node($0) }) - case "name": - return .node(name) - case "arguments": - guard !arguments.isEmpty else { - return nil - } - return .array(arguments) - case "directives": - guard !directives.isEmpty else { - return nil - } - return .array(directives) - case "selectionSet": - return selectionSet.map({ .node($0) }) - default: - return nil + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.alias) + descender.descend(&self, \.name) + descender.descend(&self, \.arguments) + descender.descend(&self, \.directives) + descender.descend(&self, \.selectionSet) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + if let alias = alias { + alias.write(to: &target) + target.write(": ") + } + name.write(to: &target) + if !arguments.isEmpty { + target.write( "(") + arguments.write(to: &target) + target.write(")") + } + if !directives.isEmpty { + target.write(" ") + directives.write(to: &target) + } + if let selectionSet = selectionSet { + target.write(" ") + selectionSet.write(to: &target) } } } @@ -548,26 +607,38 @@ extension Field : Equatable { } } -public final class Argument { +public struct Argument { public let kind: Kind = .argument public let loc: Location? - public let name: Name - public let value: Value + public var name: Name + public var value: Value init(loc: Location? = nil, name: Name, value: Value) { self.loc = loc self.name = name self.value = value } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.value) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + name.write(to: &target) + target.write(": ") + value.write(to: &target) + } +} - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return .node(name) - case "value": - return .node(value) - default: - return nil +extension Array where Element == Argument { + func write(to target: inout Target) where Target : TextOutputStream { + if let first = first { + first.write(to: &target) + } + suffix(from: 1).forEach { + target.write(", ") + $0.write(to: &target) } } } @@ -579,15 +650,11 @@ extension Argument : Equatable { } } -public protocol Fragment : Selection {} -extension FragmentSpread : Fragment {} -extension InlineFragment : Fragment {} - -public final class FragmentSpread { +public struct FragmentSpread { public let kind: Kind = .fragmentSpread public let loc: Location? - public let name: Name - public let directives: [Directive] + public var name: Name + public var directives: [Directive] init(loc: Location? = nil, name: Name, directives: [Directive] = []) { self.loc = loc @@ -595,17 +662,17 @@ public final class FragmentSpread { self.directives = directives } - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return .node(name) - case "directives": - guard !directives.isEmpty else { - return nil - } - return .array(directives) - default: - return nil + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("...") + name.write(to: &target) + if !directives.isEmpty { + target.write(" ") + directives.write(to: &target) } } } @@ -633,12 +700,12 @@ extension FragmentDefinition : HasTypeCondition { } } -public final class InlineFragment { +public struct InlineFragment { public let kind: Kind = .inlineFragment public let loc: Location? - public let typeCondition: NamedType? - public let directives: [Directive] - public let selectionSet: SelectionSet + public var typeCondition: NamedType? + public var directives: [Directive] + public var selectionSet: SelectionSet init(loc: Location? = nil, typeCondition: NamedType? = nil, directives: [Directive] = [], selectionSet: SelectionSet) { self.loc = loc @@ -646,23 +713,25 @@ public final class InlineFragment { self.directives = directives self.selectionSet = selectionSet } -} - -extension InlineFragment { - public func get(key: String) -> NodeResult? { - switch key { - case "typeCondition": - return typeCondition.map({ .node($0) }) - case "directives": - guard !directives.isEmpty else { - return nil - } - return .array(directives) - case "selectionSet": - return .node(selectionSet) - default: - return nil + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.typeCondition) + descender.descend(&self, \.directives) + descender.descend(&self, \.selectionSet) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("...") + if let typeCondition = typeCondition { + target.write(" on ") + typeCondition.write(to: &target) } + if !directives.isEmpty { + target.write(" ") + directives.write(to: &target) + } + target.write(" ") + selectionSet.write(to: &target) } } @@ -674,13 +743,13 @@ extension InlineFragment : Equatable { } } -public final class FragmentDefinition { +public struct FragmentDefinition { public let kind: Kind = .fragmentDefinition public let loc: Location? - public let name: Name - public let typeCondition: NamedType - public let directives: [Directive] - public let selectionSet: SelectionSet + public var name: Name + public var typeCondition: NamedType + public var directives: [Directive] + public var selectionSet: SelectionSet init(loc: Location? = nil, name: Name, typeCondition: NamedType, directives: [Directive] = [], selectionSet: SelectionSet) { self.loc = loc @@ -690,30 +759,28 @@ public final class FragmentDefinition { self.selectionSet = selectionSet } - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return .node(name) - case "typeCondition": - return .node(typeCondition) - case "directives": - guard !directives.isEmpty else { - return nil - } - return .array(directives) - case "selectionSet": - return .node(selectionSet) - default: - return nil + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.typeCondition) + descender.descend(&self, \.directives) + descender.descend(&self, \.selectionSet) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("fragment ") + name.write(to: &target) + target.write(" on ") + typeCondition.write(to: &target) + if !directives.isEmpty { + target.write(" ") + directives.write(to: &target) } + target.write(" ") + selectionSet.write(to: &target) } } -extension FragmentDefinition : Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - +extension FragmentDefinition: Equatable { public static func == (lhs: FragmentDefinition, rhs: FragmentDefinition) -> Bool { return lhs.name == rhs.name && lhs.typeCondition == rhs.typeCondition && @@ -722,63 +789,74 @@ extension FragmentDefinition : Hashable { } } -public protocol Value : Node {} -extension Variable : Value {} -extension IntValue : Value {} -extension FloatValue : Value {} -extension StringValue : Value {} -extension BooleanValue : Value {} -extension NullValue : Value {} -extension EnumValue : Value {} -extension ListValue : Value {} -extension ObjectValue : Value {} - -public func == (lhs: Value, rhs: Value) -> Bool { - switch lhs { - case let l as Variable: - if let r = rhs as? Variable { - return l == r - } - case let l as IntValue: - if let r = rhs as? IntValue { - return l == r - } - case let l as FloatValue: - if let r = rhs as? FloatValue { - return l == r - } - case let l as StringValue: - if let r = rhs as? StringValue { - return l == r - } - case let l as BooleanValue: - if let r = rhs as? BooleanValue { - return l == r - } - case let l as NullValue: - if let r = rhs as? NullValue { - return l == r - } - case let l as EnumValue: - if let r = rhs as? EnumValue { - return l == r +public enum Value: EnumNode, Equatable { + var underlyingNode: Node { + switch self { + case let .variable(x): + return x + case let .intValue(x): + return x + case let .floatValue(x): + return x + case let .stringValue(x): + return x + case let .booleanValue(x): + return x + case let .nullValue(x): + return x + case let .enumValue(x): + return x + case let .listValue(x): + return x + case let .objectValue(x): + return x } - case let l as ListValue: - if let r = rhs as? ListValue { - return l == r - } - case let l as ObjectValue: - if let r = rhs as? ObjectValue { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .variable(x): + descender.descend(enumCase: &x) + self = .variable(x) + case var .intValue(x): + descender.descend(enumCase: &x) + self = .intValue(x) + case var .floatValue(x): + descender.descend(enumCase: &x) + self = .floatValue(x) + case var .stringValue(x): + descender.descend(enumCase: &x) + self = .stringValue(x) + case var .booleanValue(x): + descender.descend(enumCase: &x) + self = .booleanValue(x) + case var .nullValue(x): + descender.descend(enumCase: &x) + self = .nullValue(x) + case var .enumValue(x): + descender.descend(enumCase: &x) + self = .enumValue(x) + case var .listValue(x): + descender.descend(enumCase: &x) + self = .listValue(x) + case var .objectValue(x): + descender.descend(enumCase: &x) + self = .objectValue(x) } - default: - return false } - - return false -} - -public final class IntValue { + + case variable(Variable) + case intValue(IntValue) + case floatValue(FloatValue) + case stringValue(StringValue) + case booleanValue(BooleanValue) + case nullValue(NullValue) + case enumValue(EnumValue) + case listValue(ListValue) + case objectValue(ObjectValue) +} + +public struct IntValue { public let kind: Kind = .intValue public let loc: Location? public let value: String @@ -787,6 +865,12 @@ public final class IntValue { self.loc = loc self.value = value } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write(value) + } } extension IntValue : Equatable { @@ -795,7 +879,7 @@ extension IntValue : Equatable { } } -public final class FloatValue { +public struct FloatValue { public let kind: Kind = .floatValue public let loc: Location? public let value: String @@ -804,6 +888,12 @@ public final class FloatValue { self.loc = loc self.value = value } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write(value) + } } extension FloatValue : Equatable { @@ -812,7 +902,7 @@ extension FloatValue : Equatable { } } -public final class StringValue { +public struct StringValue { public let kind: Kind = .stringValue public let loc: Location? public let value: String @@ -823,6 +913,19 @@ public final class StringValue { self.value = value self.block = block } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + if block ?? false { + //TODO: Implement this! + fatalError("Needs implemented") + } else { + target.write("\"") + target.write(value) + target.write("\"") + } + } } extension StringValue : Equatable { @@ -831,7 +934,7 @@ extension StringValue : Equatable { } } -public final class BooleanValue { +public struct BooleanValue { public let kind: Kind = .booleanValue public let loc: Location? public let value: Bool @@ -840,6 +943,12 @@ public final class BooleanValue { self.loc = loc self.value = value } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write(value ? "true" : "false") + } } extension BooleanValue : Equatable { @@ -848,13 +957,19 @@ extension BooleanValue : Equatable { } } -public final class NullValue { +public struct NullValue { public let kind: Kind = .nullValue public let loc: Location? init(loc: Location? = nil) { self.loc = loc } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("null") + } } extension NullValue : Equatable { @@ -863,7 +978,7 @@ extension NullValue : Equatable { } } -public final class EnumValue { +public struct EnumValue { public let kind: Kind = .enumValue public let loc: Location? public let value: String @@ -872,6 +987,12 @@ public final class EnumValue { self.loc = loc self.value = value } + + public mutating func descend(descender: inout Descender) { } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write(value) + } } extension EnumValue : Equatable { @@ -880,15 +1001,30 @@ extension EnumValue : Equatable { } } -public final class ListValue { +public struct ListValue { public let kind: Kind = .listValue public let loc: Location? - public let values: [Value] + public var values: [Value] init(loc: Location? = nil, values: [Value]) { self.loc = loc self.values = values } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.values) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("[") + if let first = values.first { + first.write(to: &target) + values.suffix(from: 1).forEach { + target.write(", ") + $0.write(to: &target) + } + } + } } extension ListValue : Equatable { @@ -907,15 +1043,31 @@ extension ListValue : Equatable { } } -public final class ObjectValue { +public struct ObjectValue { public let kind: Kind = .objectValue public let loc: Location? - public let fields: [ObjectField] + public var fields: [ObjectField] init(loc: Location? = nil, fields: [ObjectField]) { self.loc = loc self.fields = fields } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.fields) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("{") + if let first = fields.first { + first.write(to: &target) + fields.suffix(from: 1).forEach { + target.write(", ") + $0.write(to: &target) + } + } + target.write("}") + } } extension ObjectValue : Equatable { @@ -924,17 +1076,28 @@ extension ObjectValue : Equatable { } } -public final class ObjectField { +public struct ObjectField { public let kind: Kind = .objectField public let loc: Location? - public let name: Name - public let value: Value + public var name: Name + public var value: Value init(loc: Location? = nil, name: Name, value: Value) { self.loc = loc self.name = name self.value = value } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.value) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + name.write(to: &target) + target.write(": ") + value.write(to: &target) + } } extension ObjectField : Equatable { @@ -944,17 +1107,32 @@ extension ObjectField : Equatable { } } -public final class Directive { +public struct Directive { public let kind: Kind = .directive public let loc: Location? - public let name: Name - public let arguments: [Argument] + public var name: Name + public var arguments: [Argument] init(loc: Location? = nil, name: Name, arguments: [Argument] = []) { self.loc = loc self.name = name self.arguments = arguments } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + descender.descend(&self, \.arguments) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("@") + name.write(to: &target) + if !arguments.isEmpty { + target.write("(") + arguments.write(to: &target) + target.write(")") + } + } } extension Directive : Equatable { @@ -964,49 +1142,65 @@ extension Directive : Equatable { } } -public protocol Type : Node {} -extension NamedType : Type {} -extension ListType : Type {} -extension NonNullType : Type {} - -public func == (lhs: Type, rhs: Type) -> Bool { - switch lhs { - case let l as NamedType: - if let r = rhs as? NamedType { - return l == r +extension Array where Element == Directive { + public func write(to target: inout Target) where Target : TextOutputStream { + if let first = first { + first.write(to: &target) + suffix(from: 1).forEach { + $0.write(to: &target) + target.write(" ") + } } - case let l as ListType: - if let r = rhs as? ListType { - return l == r + } +} + +public indirect enum Type: EnumNode, Equatable { + var underlyingNode: Node { + switch self { + case let .namedType(x): + return x + case let .listType(x): + return x + case let .nonNullType(x): + return x } - case let l as NonNullType: - if let r = rhs as? NonNullType { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .namedType(x): + descender.descend(enumCase: &x) + self = .namedType(x) + case var .listType(x): + descender.descend(enumCase: &x) + self = .listType(x) + case var .nonNullType(x): + descender.descend(enumCase: &x) + self = .nonNullType(x) } - default: - return false } - - return false + + case namedType(NamedType) + case listType(ListType) + case nonNullType(NonNullType) } -public final class NamedType { +public struct NamedType { public let kind: Kind = .namedType public let loc: Location? - public let name: Name + public var name: Name init(loc: Location? = nil, name: Name) { self.loc = loc self.name = name } - public func get(key: String) -> NodeResult? { - switch key { - case "name": - return .node(name) - default: - return nil - } + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.name) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + name.write(to: &target) } } @@ -1016,15 +1210,25 @@ extension NamedType : Equatable { } } -public final class ListType { +public struct ListType { public let kind: Kind = .listType public let loc: Location? - public let type: Type + public var type: Type init(loc: Location? = nil, type: Type) { self.loc = loc self.type = type } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.type) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + target.write("[") + type.write(to: &target) + target.write("]") + } } extension ListType : Equatable { @@ -1033,74 +1237,81 @@ extension ListType : Equatable { } } -public protocol NonNullableType : Type {} -extension ListType : NonNullableType {} -extension NamedType : NonNullableType {} - -public final class NonNullType { - public let kind: Kind = .nonNullType - public let loc: Location? - public let type: NonNullableType - - init(loc: Location? = nil, type: NonNullableType) { - self.loc = loc - self.type = type +public enum NonNullType: EnumNode, Equatable { + var underlyingNode: Node { + switch self { + case let .namedType(x): + return x + case let .listType(x): + return x + } } - - public func get(key: String) -> NodeResult? { - switch key { - case "type": - return .node(type) - default: - return nil + + public mutating func descend(descender: inout Descender) { + switch self { + case var .namedType(x): + descender.descend(enumCase: &x) + self = .namedType(x) + case var .listType(x): + descender.descend(enumCase: &x) + self = .listType(x) } } -} - -extension NonNullType : Equatable { - public static func == (lhs: NonNullType, rhs: NonNullType) -> Bool { - return lhs.type == rhs.type + + case namedType(NamedType) + case listType(ListType) + + var type: Type { + switch self { + case let .namedType(x): + return .namedType(x) + case let .listType(x): + return .listType(x) + } + } + + public func write(to target: inout Target) where Target : TextOutputStream { + type.write(to: &target) + target.write("!") } } -// Type System Definition -// experimental non-spec addition. -public protocol TypeSystemDefinition : Definition {} -extension SchemaDefinition : TypeSystemDefinition {} -extension TypeExtensionDefinition : TypeSystemDefinition {} -extension DirectiveDefinition : TypeSystemDefinition {} - -public func == (lhs: TypeSystemDefinition, rhs: TypeSystemDefinition) -> Bool { - switch lhs { - case let l as SchemaDefinition: - if let r = rhs as? SchemaDefinition { - return l == r - } - case let l as TypeExtensionDefinition: - if let r = rhs as? TypeExtensionDefinition { - return l == r - } - case let l as DirectiveDefinition: - if let r = rhs as? DirectiveDefinition { - return l == r +public enum TypeSystemDefinition: EnumNode, Equatable { + var underlyingNode: Node { + switch self { + case let .schemaDefinition(x): + return x + case let .typeDefinition(x): + return x + case let .directiveDefinition(x): + return x } - case let l as TypeDefinition: - if let r = rhs as? TypeDefinition { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .schemaDefinition(x): + descender.descend(enumCase: &x) + self = .schemaDefinition(x) + case var .typeDefinition(x): + descender.descend(enumCase: &x) + self = .typeDefinition(x) + case var .directiveDefinition(x): + descender.descend(enumCase: &x) + self = .directiveDefinition(x) } - default: - return false } - - return false + + case schemaDefinition(SchemaDefinition) + case typeDefinition(TypeDefinition) + case directiveDefinition(DirectiveDefinition) } -public final class SchemaDefinition { - public let kind: Kind = .schemaDefinition +public struct SchemaDefinition { public let loc: Location? - public let description: StringValue? - public let directives: [Directive] - public let operationTypes: [OperationTypeDefinition] + public var description: StringValue? + public var directives: [Directive] + public var operationTypes: [OperationTypeDefinition] init(loc: Location? = nil, description: StringValue? = nil, directives: [Directive], operationTypes: [OperationTypeDefinition]) { self.loc = loc @@ -1108,6 +1319,16 @@ public final class SchemaDefinition { self.directives = directives self.operationTypes = operationTypes } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.directives) + descender.descend(&self, \.operationTypes) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension SchemaDefinition : Equatable { @@ -1118,17 +1339,25 @@ extension SchemaDefinition : Equatable { } } -public final class OperationTypeDefinition { +public struct OperationTypeDefinition { public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType - public let type: NamedType + public var type: NamedType init(loc: Location? = nil, operation: OperationType, type: NamedType) { self.loc = loc self.operation = operation self.type = type } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.type) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension OperationTypeDefinition : Equatable { @@ -1138,53 +1367,61 @@ extension OperationTypeDefinition : Equatable { } } -public protocol TypeDefinition : TypeSystemDefinition {} -extension ScalarTypeDefinition : TypeDefinition {} -extension ObjectTypeDefinition : TypeDefinition {} -extension InterfaceTypeDefinition : TypeDefinition {} -extension UnionTypeDefinition : TypeDefinition {} -extension EnumTypeDefinition : TypeDefinition {} -extension InputObjectTypeDefinition : TypeDefinition {} - -public func == (lhs: TypeDefinition, rhs: TypeDefinition) -> Bool { - switch lhs { - case let l as ScalarTypeDefinition: - if let r = rhs as? ScalarTypeDefinition { - return l == r - } - case let l as ObjectTypeDefinition: - if let r = rhs as? ObjectTypeDefinition { - return l == r - } - case let l as InterfaceTypeDefinition: - if let r = rhs as? InterfaceTypeDefinition { - return l == r - } - case let l as UnionTypeDefinition: - if let r = rhs as? UnionTypeDefinition { - return l == r - } - case let l as EnumTypeDefinition: - if let r = rhs as? EnumTypeDefinition { - return l == r +public enum TypeDefinition: EnumNode, Equatable { + case scalarTypeDefinition(ScalarTypeDefinition) + case objectTypeDefinition(ObjectTypeDefinition) + case interfaceTypeDefinition(InterfaceTypeDefinition) + case unionTypeDefinition(UnionTypeDefinition) + case enumTypeDefinition(EnumTypeDefinition) + case inputObjectTypeDefinition(InputObjectTypeDefinition) + + fileprivate var underlyingNode: Node { + switch self { + case let .scalarTypeDefinition(x): + return x + case let .objectTypeDefinition(x): + return x + case let .interfaceTypeDefinition(x): + return x + case let .unionTypeDefinition(x): + return x + case let .enumTypeDefinition(x): + return x + case let .inputObjectTypeDefinition(x): + return x } - case let l as InputObjectTypeDefinition: - if let r = rhs as? InputObjectTypeDefinition { - return l == r + } + + public mutating func descend(descender: inout Descender) { + switch self { + case var .scalarTypeDefinition(x): + descender.descend(enumCase: &x) + self = .scalarTypeDefinition(x) + case var .objectTypeDefinition(x): + descender.descend(enumCase: &x) + self = .objectTypeDefinition(x) + case var .interfaceTypeDefinition(x): + descender.descend(enumCase: &x) + self = .interfaceTypeDefinition(x) + case var .unionTypeDefinition(x): + descender.descend(enumCase: &x) + self = .unionTypeDefinition(x) + case var .enumTypeDefinition(x): + descender.descend(enumCase: &x) + self = .enumTypeDefinition(x) + case var .inputObjectTypeDefinition(x): + descender.descend(enumCase: &x) + self = .inputObjectTypeDefinition(x) } - default: - return false } - - return false } -public final class ScalarTypeDefinition { +public struct ScalarTypeDefinition { public let kind: Kind = .scalarTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let directives: [Directive] + public var description: StringValue? + public var name: Name + public var directives: [Directive] init(loc: Location? = nil, description: StringValue? = nil, name: Name, directives: [Directive] = []) { self.loc = loc @@ -1192,6 +1429,16 @@ public final class ScalarTypeDefinition { self.name = name self.directives = directives } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension ScalarTypeDefinition : Equatable { @@ -1202,14 +1449,14 @@ extension ScalarTypeDefinition : Equatable { } } -public final class ObjectTypeDefinition { +public struct ObjectTypeDefinition { public let kind: Kind = .objectTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let interfaces: [NamedType] - public let directives: [Directive] - public let fields: [FieldDefinition] + public var description: StringValue? + public var name: Name + public var interfaces: [NamedType] + public var directives: [Directive] + public var fields: [FieldDefinition] init(loc: Location? = nil, description: StringValue? = nil, name: Name, interfaces: [NamedType] = [], directives: [Directive] = [], fields: [FieldDefinition] = []) { self.loc = loc @@ -1219,6 +1466,18 @@ public final class ObjectTypeDefinition { self.directives = directives self.fields = fields } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.interfaces) + descender.descend(&self, \.directives) + descender.descend(&self, \.fields) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension ObjectTypeDefinition : Equatable { @@ -1231,14 +1490,14 @@ extension ObjectTypeDefinition : Equatable { } } -public final class FieldDefinition { +public struct FieldDefinition { public let kind: Kind = .fieldDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let arguments: [InputValueDefinition] - public let type: Type - public let directives: [Directive] + public var description: StringValue? + public var name: Name + public var arguments: [InputValueDefinition] + public var type: Type + public var directives: [Directive] init(loc: Location? = nil, description: StringValue? = nil, name: Name, arguments: [InputValueDefinition] = [], type: Type, directives: [Directive] = []) { self.loc = loc @@ -1248,6 +1507,18 @@ public final class FieldDefinition { self.type = type self.directives = directives } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.arguments) + descender.descend(&self, \.type) + descender.descend(&self, \.directives) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension FieldDefinition : Equatable { @@ -1260,14 +1531,14 @@ extension FieldDefinition : Equatable { } } -public final class InputValueDefinition { +public struct InputValueDefinition { public let kind: Kind = .inputValueDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let type: Type - public let defaultValue: Value? - public let directives: [Directive] + public var description: StringValue? + public var name: Name + public var type: Type + public var defaultValue: Value? + public var directives: [Directive] init(loc: Location? = nil, description: StringValue? = nil, name: Name, type: Type, defaultValue: Value? = nil, directives: [Directive] = []) { self.loc = loc @@ -1277,6 +1548,18 @@ public final class InputValueDefinition { self.defaultValue = defaultValue self.directives = directives } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.type) + descender.descend(&self, \.defaultValue) + descender.descend(&self, \.directives) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension InputValueDefinition : Equatable { @@ -1305,14 +1588,14 @@ extension InputValueDefinition : Equatable { } } -public final class InterfaceTypeDefinition { +public struct InterfaceTypeDefinition { public let kind: Kind = .interfaceTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let interfaces: [NamedType] - public let directives: [Directive] - public let fields: [FieldDefinition] + public var description: StringValue? + public var name: Name + public var interfaces: [NamedType] + public var directives: [Directive] + public var fields: [FieldDefinition] init( loc: Location? = nil, @@ -1329,6 +1612,18 @@ public final class InterfaceTypeDefinition { self.directives = directives self.fields = fields } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.interfaces) + descender.descend(&self, \.directives) + descender.descend(&self, \.fields) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension InterfaceTypeDefinition : Equatable { @@ -1340,13 +1635,13 @@ extension InterfaceTypeDefinition : Equatable { } } -public final class UnionTypeDefinition { +public struct UnionTypeDefinition { public let kind: Kind = .unionTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let directives: [Directive] - public let types: [NamedType] + public var description: StringValue? + public var name: Name + public var directives: [Directive] + public var types: [NamedType] init(loc: Location? = nil, description: StringValue? = nil, name: Name, directives: [Directive] = [], types: [NamedType]) { self.loc = loc @@ -1355,6 +1650,17 @@ public final class UnionTypeDefinition { self.directives = directives self.types = types } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + descender.descend(&self, \.types) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension UnionTypeDefinition : Equatable { @@ -1366,13 +1672,13 @@ extension UnionTypeDefinition : Equatable { } } -public final class EnumTypeDefinition { +public struct EnumTypeDefinition { public let kind: Kind = .enumTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let directives: [Directive] - public let values: [EnumValueDefinition] + public var description: StringValue? + public var name: Name + public var directives: [Directive] + public var values: [EnumValueDefinition] init(loc: Location? = nil, description: StringValue? = nil, name: Name, directives: [Directive] = [], values: [EnumValueDefinition]) { self.loc = loc @@ -1381,6 +1687,17 @@ public final class EnumTypeDefinition { self.directives = directives self.values = values } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + descender.descend(&self, \.values) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension EnumTypeDefinition : Equatable { @@ -1392,12 +1709,12 @@ extension EnumTypeDefinition : Equatable { } } -public final class EnumValueDefinition { +public struct EnumValueDefinition { public let kind: Kind = .enumValueDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let directives: [Directive] + public var description: StringValue? + public var name: Name + public var directives: [Directive] init(loc: Location? = nil, description: StringValue? = nil, name: Name, directives: [Directive] = []) { self.loc = loc @@ -1405,6 +1722,16 @@ public final class EnumValueDefinition { self.name = name self.directives = directives } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension EnumValueDefinition : Equatable { @@ -1415,13 +1742,13 @@ extension EnumValueDefinition : Equatable { } } -public final class InputObjectTypeDefinition { +public struct InputObjectTypeDefinition { public let kind: Kind = .inputObjectTypeDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let directives: [Directive] - public let fields: [InputValueDefinition] + public var description: StringValue? + public var name: Name + public var directives: [Directive] + public var fields: [InputValueDefinition] init(loc: Location? = nil, description: StringValue? = nil, name: Name, directives: [Directive] = [], fields: [InputValueDefinition]) { self.loc = loc @@ -1430,6 +1757,17 @@ public final class InputObjectTypeDefinition { self.directives = directives self.fields = fields } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.directives) + descender.descend(&self, \.fields) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension InputObjectTypeDefinition : Equatable { @@ -1441,15 +1779,23 @@ extension InputObjectTypeDefinition : Equatable { } } -public final class TypeExtensionDefinition { +public struct TypeExtensionDefinition { public let kind: Kind = .typeExtensionDefinition public let loc: Location? - public let definition: ObjectTypeDefinition + public var definition: ObjectTypeDefinition init(loc: Location? = nil, definition: ObjectTypeDefinition) { self.loc = loc self.definition = definition } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.definition) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension TypeExtensionDefinition : Equatable { @@ -1458,13 +1804,13 @@ extension TypeExtensionDefinition : Equatable { } } -public final class DirectiveDefinition { +public struct DirectiveDefinition { public let kind: Kind = .directiveDefinition public let loc: Location? - public let description: StringValue? - public let name: Name - public let arguments: [InputValueDefinition] - public let locations: [Name] + public var description: StringValue? + public var name: Name + public var arguments: [InputValueDefinition] + public var locations: [Name] init(loc: Location? = nil, description: StringValue? = nil, name: Name, arguments: [InputValueDefinition] = [], locations: [Name]) { self.loc = loc @@ -1473,6 +1819,17 @@ public final class DirectiveDefinition { self.arguments = arguments self.locations = locations } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.description) + descender.descend(&self, \.name) + descender.descend(&self, \.arguments) + descender.descend(&self, \.locations) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + fatalError("TODO") + } } extension DirectiveDefinition : Equatable { diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 7c19ed71..1b382f85 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -168,24 +168,24 @@ func parseDocument(lexer: Lexer) throws -> Document { */ func parseDefinition(lexer: Lexer) throws -> Definition { if peek(lexer: lexer, kind: .openingBrace) { - return try parseOperationDefinition(lexer: lexer) + return .executableDefinition(.operation(try parseOperationDefinition(lexer: lexer))) } if peek(lexer: lexer, kind: .name) { switch lexer.token.value! { // Note: subscription is an experimental non-spec addition. case "query", "mutation", "subscription": - return try parseOperationDefinition(lexer: lexer); + return .executableDefinition(.operation(try parseOperationDefinition(lexer: lexer))) case "fragment": - return try parseFragmentDefinition(lexer: lexer) + return .executableDefinition(.fragment(try parseFragmentDefinition(lexer: lexer))) // Note: the Type System IDL is an experimental non-spec addition. case "schema", "scalar", "type", "interface", "union", "enum", "input", "extend", "directive": - return try parseTypeSystemDefinition(lexer: lexer) + return .typeSystemDefinitionOrExtension(.typeSystemDefinition(try parseTypeSystemDefinition(lexer: lexer))) default: break } } else if peekDescription(lexer: lexer) { - return try parseTypeSystemDefinition(lexer: lexer) + return .typeSystemDefinitionOrExtension(.typeSystemDefinition(try parseTypeSystemDefinition(lexer: lexer))) } throw unexpected(lexer: lexer) @@ -318,7 +318,7 @@ func parseSelection(lexer: Lexer) throws -> Selection { * * Alias : Name : */ -func parseField(lexer: Lexer) throws -> Field { +func parseField(lexer: Lexer) throws -> Selection { let start = lexer.token; let nameOrAlias = try parseName(lexer: lexer) @@ -333,13 +333,15 @@ func parseField(lexer: Lexer) throws -> Field { name = nameOrAlias } - return Field( - loc: loc(lexer: lexer, startToken: start), - alias: alias, - name: name, - arguments: try parseArguments(lexer: lexer), - directives: try parseDirectives(lexer: lexer), - selectionSet: peek(lexer: lexer, kind: .openingBrace) ? try parseSelectionSet(lexer: lexer) : nil + return .field( + Field( + loc: loc(lexer: lexer, startToken: start), + alias: alias, + name: name, + arguments: try parseArguments(lexer: lexer), + directives: try parseDirectives(lexer: lexer), + selectionSet: peek(lexer: lexer, kind: .openingBrace) ? try parseSelectionSet(lexer: lexer) : nil + ) ) } @@ -378,14 +380,16 @@ func parseArgument(lexer: Lexer) throws -> Argument { * * InlineFragment : ... TypeCondition? Directives? SelectionSet */ -func parseFragment(lexer: Lexer) throws -> Fragment { +func parseFragment(lexer: Lexer) throws -> Selection { let start = lexer.token try expect(lexer: lexer, kind: .spread) if peek(lexer: lexer, kind: .name) && lexer.token.value != "on" { - return FragmentSpread( - loc: loc(lexer: lexer, startToken: start), - name: try parseFragmentName(lexer: lexer), - directives: try parseDirectives(lexer: lexer) + return .fragmentSpread( + FragmentSpread( + loc: loc(lexer: lexer, startToken: start), + name: try parseFragmentName(lexer: lexer), + directives: try parseDirectives(lexer: lexer) + ) ) } @@ -395,11 +399,13 @@ func parseFragment(lexer: Lexer) throws -> Fragment { try lexer.advance() typeCondition = try parseNamedType(lexer: lexer) } - return InlineFragment( - loc: loc(lexer: lexer, startToken: start), - typeCondition: typeCondition, - directives: try parseDirectives(lexer: lexer), - selectionSet: try parseSelectionSet(lexer: lexer) + return .inlineFragment( + InlineFragment( + loc: loc(lexer: lexer, startToken: start), + typeCondition: typeCondition, + directives: try parseDirectives(lexer: lexer), + selectionSet: try parseSelectionSet(lexer: lexer) + ) ) } @@ -453,45 +459,45 @@ func parseValueLiteral(lexer: Lexer, isConst: Bool) throws -> Value { let token = lexer.token switch token.kind { case .openingBracket: - return try parseList(lexer: lexer, isConst: isConst) + return .listValue(try parseList(lexer: lexer, isConst: isConst)) case .openingBrace: - return try parseObject(lexer: lexer, isConst: isConst) + return .objectValue(try parseObject(lexer: lexer, isConst: isConst)) case .int: try lexer.advance() - return IntValue( + return .intValue(IntValue( loc: loc(lexer: lexer, startToken: token), value: token.value! - ) + )) case .float: try lexer.advance() - return FloatValue( + return .floatValue(FloatValue( loc: loc(lexer: lexer, startToken: token), value: token.value! - ) + )) case .string, .blockstring: - return try parseStringLiteral(lexer: lexer, startToken: token) + return .stringValue(try parseStringLiteral(lexer: lexer, startToken: token)) case .name: if (token.value == "true" || token.value == "false") { try lexer.advance() - return BooleanValue( + return .booleanValue(BooleanValue( loc: loc(lexer: lexer, startToken: token), value: token.value == "true" - ) + )) } else if token.value == "null" { try lexer.advance() - return NullValue( + return .nullValue(NullValue( loc: loc(lexer: lexer, startToken: token) - ) + )) } else { try lexer.advance() - return EnumValue( + return .enumValue(EnumValue( loc: loc(lexer: lexer, startToken: token), value: token.value! - ) + )) } case .dollar: if !isConst { - return try parseVariable(lexer: lexer) + return .variable(try parseVariable(lexer: lexer)) } default: break @@ -616,19 +622,23 @@ func parseTypeReference(lexer: Lexer) throws -> Type { if try skip(lexer: lexer, kind: .openingBracket) { type = try parseTypeReference(lexer: lexer) try expect(lexer: lexer, kind: .closingBracket) - type = ListType( + type = .listType(ListType( loc: loc(lexer: lexer, startToken: start), type: type - ) + )) } else { - type = try parseNamedType(lexer: lexer) + type = .namedType(try parseNamedType(lexer: lexer)) } if try skip(lexer: lexer, kind: .bang) { - return NonNullType( - loc: loc(lexer: lexer, startToken: start), - type: type as! NonNullableType - ) + switch type { + case let .namedType(x): + return .nonNullType(.namedType(x)) + case let .listType(x): + return .nonNullType(.listType(x)) + default: + fatalError() + } } return type @@ -666,22 +676,31 @@ func parseTypeSystemDefinition(lexer: Lexer) throws -> TypeSystemDefinition { let keywordToken = peekDescription(lexer: lexer) ? try lexer.lookahead() : lexer.token - + if keywordToken.kind == .name { switch keywordToken.value! { - case "schema": return try parseSchemaDefinition(lexer: lexer); - case "scalar": return try parseScalarTypeDefinition(lexer: lexer); - case "type": return try parseObjectTypeDefinition(lexer: lexer); - case "interface": return try parseInterfaceTypeDefinition(lexer: lexer); - case "union": return try parseUnionTypeDefinition(lexer: lexer); - case "enum": return try parseEnumTypeDefinition(lexer: lexer); - case "input": return try parseInputObjectTypeDefinition(lexer: lexer); - case "extend": return try parseTypeExtensionDefinition(lexer: lexer); - case "directive": return try parseDirectiveDefinition(lexer: lexer); - default: break + case "schema": + return .schemaDefinition(try parseSchemaDefinition(lexer: lexer)) + case "scalar": + return .typeDefinition(.scalarTypeDefinition(try parseScalarTypeDefinition(lexer: lexer))) + case "type": + return .typeDefinition(.objectTypeDefinition(try parseObjectTypeDefinition(lexer: lexer))) + case "interface": + return .typeDefinition(.interfaceTypeDefinition(try parseInterfaceTypeDefinition(lexer: lexer))) + case "union": + return .typeDefinition(.unionTypeDefinition(try parseUnionTypeDefinition(lexer: lexer))) + case "enum": + return .typeDefinition(.enumTypeDefinition(try parseEnumTypeDefinition(lexer: lexer))) + case "input": + return .typeDefinition(.inputObjectTypeDefinition(try parseInputObjectTypeDefinition(lexer: lexer))) +// case "extend": +// return try parseTypeExtensionDefinition(lexer: lexer); + case "directive": + return .directiveDefinition(try parseDirectiveDefinition(lexer: lexer)) + default: + break } } - throw unexpected(lexer: lexer, atToken: keywordToken) } diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 0057c556..95412365 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -1,50 +1,133 @@ -let QueryDocumentKeys: [Kind: [String]] = [ - .name: [], - - .document: ["definitions"], - .operationDefinition: ["name", "variableDefinitions", "directives", "selectionSet"], - .variableDefinition: ["variable", "type", "defaultValue"], - .variable: ["name"], - .selectionSet: ["selections"], - .field: ["alias", "name", "arguments", "directives", "selectionSet"], - .argument: ["name", "value"], - - .fragmentSpread: ["name", "directives"], - .inlineFragment: ["typeCondition", "directives", "selectionSet"], - .fragmentDefinition: ["name", "typeCondition", "directives", "selectionSet"], - - .intValue: [], - .floatValue: [], - .stringValue: [], - .booleanValue: [], - .enumValue: [], - .listValue: ["values"], - .objectValue: ["fields"], - .objectField: ["name", "value"], - - .directive: ["name", "arguments"], - - .namedType: ["name"], - .listType: ["type"], - .nonNullType: ["type"], - - .schemaDefinition: ["directives", "operationTypes"], - .operationTypeDefinition: ["type"], - - .scalarTypeDefinition: ["name", "directives"], - .objectTypeDefinition: ["name", "interfaces", "directives", "fields"], - .fieldDefinition: ["name", "arguments", "type", "directives"], - .inputValueDefinition: ["name", "type", "defaultValue", "directives"], - .interfaceTypeDefinition: ["name", "interfaces", "directives", "fields"], - .unionTypeDefinition: ["name", "directives", "types"], - .enumTypeDefinition: ["name", "directives", "values"], - .enumValueDefinition: ["name", "directives"], - .inputObjectTypeDefinition: ["name", "directives", "fields"], - - .typeExtensionDefinition: ["definition"], - - .directiveDefinition: ["name", "arguments", "locations"], -] +public struct Descender { + + enum Mutation { + case replace(T) + case remove + } + private let visitor: Visitor + + fileprivate var parentStack: [VisitorParent] = [] + private var path: [AnyKeyPath] = [] + private var isBreaking = false + + fileprivate mutating func go(node: inout H, key: AnyKeyPath?) -> Mutation? { + if isBreaking { return nil } + let parent = parentStack.last + let newPath: [AnyKeyPath] + if let key = key { + newPath = path + [key] + } else { + newPath = path + } + + var mutation: Mutation? = nil + + switch visitor.enter(node: node, key: key, parent: parent, path: newPath, ancestors: parentStack) { + case .skip: + return nil// .node(Optional(result)) + case .continue: + break + case let .node(newNode): + if let newNode = newNode { + mutation = .replace(newNode) + } else { + // TODO: Should we still be traversing the children here? + mutation = .remove + } + case .break: + isBreaking = true + return nil + } + parentStack.append(.node(node)) + if let key = key { + path.append(key) + } + node.descend(descender: &self) + if key != nil { + path.removeLast() + } + parentStack.removeLast() + + if isBreaking { return mutation } + + switch visitor.leave(node: node, key: key, parent: parent, path: newPath, ancestors: parentStack) { + case .skip, .continue: + return mutation + case let .node(newNode): + if let newNode = newNode { + return .replace(newNode) + } else { + // TODO: Should we still be traversing the children here? + return .remove + } + case .break: + isBreaking = true + return mutation + } + } + + + mutating func descend(_ node: inout T, _ kp: WritableKeyPath) { + switch go(node: &node[keyPath: kp], key: kp) { + case nil: + break + case .replace(let child): + node[keyPath: kp] = child + case .remove: + fatalError("Can't remove this node") + } + } + mutating func descend(_ node: inout T, _ kp: WritableKeyPath) { + guard var oldVal = node[keyPath: kp] else { + return + } + switch go(node: &oldVal, key: kp) { + case nil: + node[keyPath: kp] = oldVal + case .replace(let child): + node[keyPath: kp] = child + case .remove: + node[keyPath: kp] = nil + } + } + mutating func descend(_ node: inout T, _ kp: WritableKeyPath) { + var toRemove: [Int] = [] + + parentStack.append(.array(node[keyPath: kp])) + + var i = node[keyPath: kp].startIndex + while i != node[keyPath: kp].endIndex { + switch go(node: &node[keyPath: kp][i], key: \[U].[i]) { + case nil: + break + case .replace(let child): + node[keyPath: kp][i] = child + case .remove: + toRemove.append(i) + } + i = node[keyPath: kp].index(after: i) + } + parentStack.removeLast() + toRemove.forEach { node[keyPath: kp].remove(at: $0) } + } + + mutating func descend(enumCase: inout T) { + switch go(node: &enumCase, key: nil) { + case nil: + break + case .replace(let node): + enumCase = node + case .remove: + //TODO: figure this out + fatalError("What happens here?") + } + } + + fileprivate init(visitor: Visitor) { + self.visitor = visitor + } +} + /** * visit() will walk through an AST using a depth first traversal, calling @@ -60,306 +143,570 @@ let QueryDocumentKeys: [Kind: [String]] = [ * a new version of the AST with the changes applied will be returned from the * visit function. * - * let editedAST = visit(ast, Visitor( - * enter: { node, key, parent, path, ancestors in + * struct MyVisitor: Visitor { + * func enter(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { * return - * .continue: no action - * .skip: skip visiting this node - * .break: stop visiting altogether - * .node(nil): delete this node - * .node(newNode): replace this node with the returned value - * }, - * leave: { node, key, parent, path, ancestors in + * .continue // no action + * .skip // skip visiting this node + * .break // stop visiting altogether + * .node(nil) // delete this node + * .node(newNode) // replace this node with the returned value + * } + * func leave(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { * return - * .continue: no action - * .skip: no action - * .break: stop visiting altogether - * .node(nil): delete this node - * .node(newNode): replace this node with the returned value + * .continue // no action + * .skip // skip visiting this node + * .break // stop visiting altogether + * .node(nil) // delete this node + * .node(newNode) // replace this node with the returned value * } - * )) + * } + * let editedAST = visit(ast, visitor: MyVisitor()) + * */ @discardableResult -func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node { - let visitorKeys = keyMap.isEmpty ? QueryDocumentKeys : keyMap - - var stack: Stack? = nil - var inArray = false - var keys: [IndexPathElement] = ["root"] - var index: Int = -1 - var edits: [(key: IndexPathElement, node: Node)] = [] - var parent: NodeResult? = nil - var path: [IndexPathElement] = [] - var ancestors: [NodeResult] = [] - var newRoot = root - - repeat { - index += 1 - let isLeaving = index == keys.count - var key: IndexPathElement? = nil - var node: NodeResult? = nil - let isEdited = isLeaving && !edits.isEmpty - - if !isLeaving { - key = parent != nil ? inArray ? index : keys[index] : nil - - if let parent = parent { - switch parent { - case .node(let parent): - node = parent.get(key: key!.keyValue!) - case .array(let parent): - node = .node(parent[key!.indexValue!]) - } - } else { - node = .node(newRoot) - } +public func visit(root: T, visitor: V) -> T { + var descender = Descender(visitor: visitor) + + var result = root + switch descender.go(node: &result, key: nil) { + case .remove: + fatalError("Root node in the AST was removed") + case .replace(let node): + return node + case nil: + return result + } +} - if node == nil { - continue - } +public enum VisitorParent { + case node(Node) + case array([Node]) - if parent != nil { - path.append(key!) - } - } else { - key = ancestors.isEmpty ? nil : path.popLast() - node = parent - parent = ancestors.popLast() - - if isEdited { -// if inArray { -// node = node.slice() -// } else { -// let clone = node -// node = clone -// } -// -// var editOffset = 0 -// -// for ii in 0.. VisitResult + func leave(name: Name, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if case .node(let n) = node! { - if !isLeaving { - result = visitor.enter( - node: n, - key: key, - parent: parent, - path: path, - ancestors: ancestors - ) - } else { - result = visitor.leave( - node: n, - key: key, - parent: parent, - path: path, - ancestors: ancestors - ) - } + func enter(document: Document, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(document: Document, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(definition: Definition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(definition: Definition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(executableDefinition: ExecutableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(executableDefinition: ExecutableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if case .break = result { - break - } + func enter(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if case .skip = result, !isLeaving { - _ = path.popLast() - continue - } else if case .node(let n) = result { - edits.append((key!, n!)) - - if !isLeaving { - if let n = n { - node = .node(n) - } else { - _ = path.popLast() - continue - } - } - } - } + func enter(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult -// if case .continue = result, isEdited { -// edits.append((key!, node!)) -// } + func enter(variable: Variable, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(variable: Variable, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if !isLeaving { - stack = Stack(index: index, keys: keys, edits: edits, inArray: inArray, prev: stack) - inArray = node!.isArray + func enter(selectionSet: SelectionSet, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(selectionSet: SelectionSet, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(selection: Selection, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(selection: Selection, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - switch node! { - case .node(let node): - keys = visitorKeys[node.kind] ?? [] - case .array(let array): - keys = array.map({ _ in "root" }) - } + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - index = -1 - edits = [] + func enter(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if let parent = parent { - ancestors.append(parent) - } + func enter(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - parent = node - } - } while stack != nil + func enter(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - if !edits.isEmpty { - newRoot = edits[edits.count - 1].node - } + func enter(fragmentDefinition: FragmentDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(fragmentDefinition: FragmentDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(value: Value, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(value: Value, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - return newRoot -} + func enter(intValue: IntValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(intValue: IntValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult -final class Stack { - let index: Int - let keys: [IndexPathElement] - let edits: [(key: IndexPathElement, node: Node)] - let inArray: Bool - let prev: Stack? - - init(index: Int, keys: [IndexPathElement], edits: [(key: IndexPathElement, node: Node)], inArray: Bool, prev: Stack?) { - self.index = index - self.keys = keys - self.edits = edits - self.inArray = inArray - self.prev = prev - } -} + func enter(floatValue: FloatValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(floatValue: FloatValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult -/** - * Creates a new visitor instance which delegates to many visitors to run in - * parallel. Each visitor will be visited for each node before moving on. - * - * If a prior visitor edits a node, no following visitors will see that node. - */ -func visitInParallel(visitors: [Visitor]) -> Visitor { - var skipping: [Node?] = [Node?](repeating: nil, count: visitors.count) - - return Visitor( - enter: { node, key, parent, path, ancestors in - for i in 0.. VisitResult + func leave(stringValue: StringValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - return .continue - }, - leave: { node, key, parent, path, ancestors in - for i in 0.. VisitResult + func leave(booleanValue: BooleanValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(nullValue: NullValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(nullValue: NullValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - return .continue - } - ) -} + func enter(enumValue: EnumValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(enumValue: EnumValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult -enum VisitResult { - case `continue` - case skip - case `break` - case node(Node?) + func enter(listValue: ListValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(listValue: ListValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult - var isContinue: Bool { - if case .continue = self { - return true - } - return false - } + func enter(objectValue: ObjectValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(objectValue: ObjectValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(objectField: ObjectField, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(objectField: ObjectField, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(directive: Directive, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(directive: Directive, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(namedType: NamedType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(namedType: NamedType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(listType: ListType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(listType: ListType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(nonNullType: NonNullType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(nonNullType: NonNullType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(schemaDefinition: SchemaDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(schemaDefinition: SchemaDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(operationTypeDefinition: OperationTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(operationTypeDefinition: OperationTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(scalarTypeDefinition: ScalarTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(scalarTypeDefinition: ScalarTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(objectTypeDefinition: ObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(objectTypeDefinition: ObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(fieldDefinition: FieldDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(fieldDefinition: FieldDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(inputValueDefinition: InputValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(inputValueDefinition: InputValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(interfaceTypeDefinition: InterfaceTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(interfaceTypeDefinition: InterfaceTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(unionTypeDefinition: UnionTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(unionTypeDefinition: UnionTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(enumTypeDefinition: EnumTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(enumTypeDefinition: EnumTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(enumValueDefinition: EnumValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(enumValueDefinition: EnumValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(inputObjectTypeDefinition: InputObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(inputObjectTypeDefinition: InputObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(typeExtensionDefinition: TypeExtensionDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(typeExtensionDefinition: TypeExtensionDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(directiveDefinition: DirectiveDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + func leave(directiveDefinition: DirectiveDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult + + func enter(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult + func leave(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult } -struct Visitor { - typealias Visit = (Node, IndexPathElement?, NodeResult?, [IndexPathElement], [NodeResult]) -> VisitResult - private let enter: Visit - private let leave: Visit +public extension Visitor { + func enter(name: Name, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(name: Name, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(document: Document, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(document: Document, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(definition: Definition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(definition: Definition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(executableDefinition: ExecutableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(executableDefinition: ExecutableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } - init(enter: @escaping Visit = ignore, leave: @escaping Visit = ignore) { - self.enter = enter - self.leave = leave - } + func enter(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(variable: Variable, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(variable: Variable, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(selectionSet: SelectionSet, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(selectionSet: SelectionSet, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(selection: Selection, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(selection: Selection, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } - func enter(node: Node, key: IndexPathElement?, parent: NodeResult?, path: [IndexPathElement], ancestors: [NodeResult]) -> VisitResult { - return enter(node, key, parent, path, ancestors) + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(fragmentDefinition: FragmentDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(fragmentDefinition: FragmentDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(value: Value, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(value: Value, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(intValue: IntValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(intValue: IntValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(floatValue: FloatValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(floatValue: FloatValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(stringValue: StringValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(stringValue: StringValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(booleanValue: BooleanValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(booleanValue: BooleanValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(nullValue: NullValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(nullValue: NullValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(enumValue: EnumValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(enumValue: EnumValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(listValue: ListValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(listValue: ListValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(objectValue: ObjectValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(objectValue: ObjectValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(objectField: ObjectField, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(objectField: ObjectField, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(directive: Directive, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(directive: Directive, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(type: Type, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(type: Type, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(namedType: NamedType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(namedType: NamedType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(listType: ListType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(listType: ListType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(nonNullType: NonNullType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(nonNullType: NonNullType, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(schemaDefinition: SchemaDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(schemaDefinition: SchemaDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(operationTypeDefinition: OperationTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(operationTypeDefinition: OperationTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(scalarTypeDefinition: ScalarTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(scalarTypeDefinition: ScalarTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(objectTypeDefinition: ObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(objectTypeDefinition: ObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(fieldDefinition: FieldDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(fieldDefinition: FieldDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(inputValueDefinition: InputValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(inputValueDefinition: InputValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(interfaceTypeDefinition: InterfaceTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(interfaceTypeDefinition: InterfaceTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(unionTypeDefinition: UnionTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(unionTypeDefinition: UnionTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(enumTypeDefinition: EnumTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(enumTypeDefinition: EnumTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(enumValueDefinition: EnumValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(enumValueDefinition: EnumValueDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(inputObjectTypeDefinition: InputObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(inputObjectTypeDefinition: InputObjectTypeDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(typeExtensionDefinition: TypeExtensionDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(typeExtensionDefinition: TypeExtensionDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(directiveDefinition: DirectiveDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + func leave(directiveDefinition: DirectiveDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { .continue } + + func enter(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + switch node { + case let name as Name: + return enter(name: name, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let document as Document: + return enter(document: document, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let definition as Definition: + return enter(definition: definition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let executableDefinition as ExecutableDefinition: + return enter(executableDefinition: executableDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let operationDefinition as OperationDefinition: + return enter(operationDefinition: operationDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let variableDefinition as VariableDefinition: + return enter(variableDefinition: variableDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let variable as Variable: + return enter(variable: variable, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let selectionSet as SelectionSet: + return enter(selectionSet: selectionSet, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let selection as Selection: + return enter(selection: selection, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let field as Field: + return enter(field: field, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let argument as Argument: + return enter(argument: argument, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fragmentSpread as FragmentSpread: + return enter(fragmentSpread: fragmentSpread, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inlineFragment as InlineFragment: + return enter(inlineFragment: inlineFragment, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fragmentDefinition as FragmentDefinition: + return enter(fragmentDefinition: fragmentDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let value as Value: + return enter(value: value, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let intValue as IntValue: + return enter(intValue: intValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let floatValue as FloatValue: + return enter(floatValue: floatValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let stringValue as StringValue: + return enter(stringValue: stringValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let booleanValue as BooleanValue: + return enter(booleanValue: booleanValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let nullValue as NullValue: + return enter(nullValue: nullValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumValue as EnumValue: + return enter(enumValue: enumValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let listValue as ListValue: + return enter(listValue: listValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectValue as ObjectValue: + return enter(objectValue: objectValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectField as ObjectField: + return enter(objectField: objectField, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let directive as Directive: + return enter(directive: directive, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let type as Type: + return enter(type: type, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let namedType as NamedType: + return enter(namedType: namedType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let listType as ListType: + return enter(listType: listType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let nonNullType as NonNullType: + return enter(nonNullType: nonNullType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let schemaDefinition as SchemaDefinition: + return enter(schemaDefinition: schemaDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let operationTypeDefinition as OperationTypeDefinition: + return enter(operationTypeDefinition: operationTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let scalarTypeDefinition as ScalarTypeDefinition: + return enter(scalarTypeDefinition: scalarTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectTypeDefinition as ObjectTypeDefinition: + return enter(objectTypeDefinition: objectTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fieldDefinition as FieldDefinition: + return enter(fieldDefinition: fieldDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inputValueDefinition as InputValueDefinition: + return enter(inputValueDefinition: inputValueDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let interfaceTypeDefinition as InterfaceTypeDefinition: + return enter(interfaceTypeDefinition: interfaceTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let unionTypeDefinition as UnionTypeDefinition: + return enter(unionTypeDefinition: unionTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumTypeDefinition as EnumTypeDefinition: + return enter(enumTypeDefinition: enumTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumValueDefinition as EnumValueDefinition: + return enter(enumValueDefinition: enumValueDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inputObjectTypeDefinition as InputObjectTypeDefinition: + return enter(inputObjectTypeDefinition: inputObjectTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let typeExtensionDefinition as TypeExtensionDefinition: + return enter(typeExtensionDefinition: typeExtensionDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let directiveDefinition as DirectiveDefinition: + return enter(directiveDefinition: directiveDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + default: + fatalError() + } } - func leave(node: Node, key: IndexPathElement?, parent: NodeResult?, path: [IndexPathElement], ancestors: [NodeResult]) -> VisitResult { - return leave(node, key, parent, path, ancestors) + func leave(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + switch node { + case let name as Name: + return leave(name: name, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let document as Document: + return leave(document: document, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let definition as Definition: + return leave(definition: definition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let executableDefinition as ExecutableDefinition: + return leave(executableDefinition: executableDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let operationDefinition as OperationDefinition: + return leave(operationDefinition: operationDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let variableDefinition as VariableDefinition: + return leave(variableDefinition: variableDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let variable as Variable: + return leave(variable: variable, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let selectionSet as SelectionSet: + return leave(selectionSet: selectionSet, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let selection as Selection: + return leave(selection: selection, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let field as Field: + return leave(field: field, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let argument as Argument: + return leave(argument: argument, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fragmentSpread as FragmentSpread: + return leave(fragmentSpread: fragmentSpread, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inlineFragment as InlineFragment: + return leave(inlineFragment: inlineFragment, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fragmentDefinition as FragmentDefinition: + return leave(fragmentDefinition: fragmentDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let value as Value: + return leave(value: value, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let intValue as IntValue: + return leave(intValue: intValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let floatValue as FloatValue: + return leave(floatValue: floatValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let stringValue as StringValue: + return leave(stringValue: stringValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let booleanValue as BooleanValue: + return leave(booleanValue: booleanValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let nullValue as NullValue: + return leave(nullValue: nullValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumValue as EnumValue: + return leave(enumValue: enumValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let listValue as ListValue: + return leave(listValue: listValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectValue as ObjectValue: + return leave(objectValue: objectValue, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectField as ObjectField: + return leave(objectField: objectField, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let directive as Directive: + return leave(directive: directive, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let type as Type: + return leave(type: type, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let namedType as NamedType: + return leave(namedType: namedType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let listType as ListType: + return leave(listType: listType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let nonNullType as NonNullType: + return leave(nonNullType: nonNullType, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let schemaDefinition as SchemaDefinition: + return leave(schemaDefinition: schemaDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let operationTypeDefinition as OperationTypeDefinition: + return leave(operationTypeDefinition: operationTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let scalarTypeDefinition as ScalarTypeDefinition: + return leave(scalarTypeDefinition: scalarTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let objectTypeDefinition as ObjectTypeDefinition: + return leave(objectTypeDefinition: objectTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let fieldDefinition as FieldDefinition: + return leave(fieldDefinition: fieldDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inputValueDefinition as InputValueDefinition: + return leave(inputValueDefinition: inputValueDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let interfaceTypeDefinition as InterfaceTypeDefinition: + return leave(interfaceTypeDefinition: interfaceTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let unionTypeDefinition as UnionTypeDefinition: + return leave(unionTypeDefinition: unionTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumTypeDefinition as EnumTypeDefinition: + return leave(enumTypeDefinition: enumTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let enumValueDefinition as EnumValueDefinition: + return leave(enumValueDefinition: enumValueDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let inputObjectTypeDefinition as InputObjectTypeDefinition: + return leave(inputObjectTypeDefinition: inputObjectTypeDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let typeExtensionDefinition as TypeExtensionDefinition: + return leave(typeExtensionDefinition: typeExtensionDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + case let directiveDefinition as DirectiveDefinition: + return leave(directiveDefinition: directiveDefinition, key: key, parent: parent, ancestors: ancestors) as! VisitResult + default: + fatalError() + } } } -func ignore(node: Node, key: IndexPathElement?, parent: NodeResult?, path: [IndexPathElement], ancestors: [NodeResult]) -> VisitResult { - return .continue -} +/** + * A visitor which maintains a provided TypeInfo instance alongside another visitor. + */ +public struct VisitorWithTypeInfo: Visitor { + let visitor: Visitor + let typeInfo: TypeInfo + public init(visitor: Visitor, typeInfo: TypeInfo) { + self.visitor = visitor + self.typeInfo = typeInfo + } + public func enter(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + typeInfo.enter(node: node) + + let result = visitor.enter( + node: node, + key: key, + parent: parent, + path: path, + ancestors: ancestors + ) + + if case .continue = result {} else { + typeInfo.leave(node: node) + if case .node(let node) = result, let n = node { + typeInfo.enter(node: n) + } + + } + return result + } + public func leave(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + let result = visitor.leave( + node: node, + key: key, + parent: parent, + path: path, + ancestors: ancestors + ) + + typeInfo.leave(node: node) + return result + } +} /** - * Creates a new visitor instance which maintains a provided TypeInfo instance - * along with visiting visitor. + A visitor which visits delegates to many visitors to run in parallel. + + Each visitor will be visited for each node before moving on. + If a prior visitor edits a node, no following visitors will see that node. */ -func visitWithTypeInfo(typeInfo: TypeInfo, visitor: Visitor) -> Visitor { - return Visitor( - enter: { node, key, parent, path, ancestors in - typeInfo.enter(node: node) - +class ParallelVisitor: Visitor { + let visitors: [Visitor] + + private var skipping: [SkipStatus] + private enum SkipStatus { + case skipping([AnyKeyPath]) + case breaking + case continuing + } + + init(visitors: [Visitor]) { + self.visitors = visitors + self.skipping = [SkipStatus](repeating: .continuing, count: visitors.count) + } + + public func enter(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + for (i, visitor) in visitors.enumerated() { + guard case .continuing = skipping[i] else { + continue + } let result = visitor.enter( node: node, key: key, @@ -367,28 +714,76 @@ func visitWithTypeInfo(typeInfo: TypeInfo, visitor: Visitor) -> Visitor { path: path, ancestors: ancestors ) - - if !result.isContinue { - typeInfo.leave(node: node) - - if case .node(let node) = result, let n = node { - typeInfo.enter(node: n) + switch result { + case .node: + return result + case .break: + skipping[i] = .breaking + case .skip: + skipping[i] = .skipping(path) + case .continue: + break + } + } + return .continue + } + public func leave(node: T, key: AnyKeyPath?, parent: VisitorParent?, path: [AnyKeyPath], ancestors: [VisitorParent]) -> VisitResult { + for (i, visitor) in visitors.enumerated() { + switch skipping[i] { + case .skipping(path): + // We've come back to leave the node we were skipping + // So unset the skipping status so that the visitor will resume traversing + skipping[i] = .continuing + case .skipping, .breaking: + break + case .continuing: + let result = visitor.leave( + node: node, + key: key, + parent: parent, + path: path, + ancestors: ancestors + ) + switch result { + case .break: + skipping[i] = .breaking + case .node: + return result + default: + break } } + } + return .continue + } +} - return result - }, - leave: { node, key, parent, path, ancestors in - let result = visitor.leave( - node: node, - key: key, - parent: parent, - path: path, - ancestors: ancestors - ) +public enum VisitResult { + case `continue`, skip, `break`, node(T?) +} - typeInfo.leave(node: node) - return result +fileprivate enum SomeVisitResult2 { + case `continue`, skip, `break`, node(Node?) + static func from(_ visitResult: VisitResult) -> SomeVisitResult2 { + switch visitResult { + case .continue: + return .continue + case .skip: + return .skip + case .break: + return .break + case .node(let node): + return .node(node) } - ) + } } + +/** + * Creates a new visitor instance which maintains a provided TypeInfo instance + * along with visiting visitor. + */ + +/** + * Creates a new visitor instance which maintains a provided TypeInfo instance + * along with visiting visitor. + */ diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 57649903..f7d15fc9 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -297,30 +297,52 @@ extension GraphQLScalarType : Hashable { public final class GraphQLObjectType { public let name: String public let description: String? - public let fields: GraphQLFieldDefinitionMap - public let interfaces: [GraphQLInterfaceType] + private let fieldsThunk: () -> GraphQLFieldMap + public lazy var fields: GraphQLFieldDefinitionMap = { + try! defineFieldMap( + name: name, + fields: fieldsThunk() + ) + }() + private let interfacesThunk: () -> [GraphQLInterfaceType] + public lazy var interfaces: [GraphQLInterfaceType] = { + try! defineInterfaces( + name: name, + hasTypeOf: isTypeOf != nil, + interfaces: interfacesThunk() + ) + }() public let isTypeOf: GraphQLIsTypeOf? public let kind: TypeKind = .object - public init( + public convenience init( name: String, description: String? = nil, fields: GraphQLFieldMap, interfaces: [GraphQLInterfaceType] = [], isTypeOf: GraphQLIsTypeOf? = nil + ) throws { + try self.init( + name: name, + description: description, + fields: { fields }, + interfaces: { interfaces }, + isTypeOf: isTypeOf + ) + } + + public init( + name: String, + description: String? = nil, + fields: @escaping () -> GraphQLFieldMap, + interfaces: @escaping () -> [GraphQLInterfaceType], + isTypeOf: GraphQLIsTypeOf? = nil ) throws { try assertValid(name: name) self.name = name self.description = description - self.fields = try defineFieldMap( - name: name, - fields: fields - ) - self.interfaces = try defineInterfaces( - name: name, - hasTypeOf: isTypeOf != nil, - interfaces: interfaces - ) + self.fieldsThunk = fields + self.interfacesThunk = interfaces self.isTypeOf = isTypeOf } @@ -339,6 +361,15 @@ extension GraphQLObjectType : Encodable { case interfaces case kind } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(fields, forKey: .fields) + try container.encode(interfaces, forKey: .interfaces) + try container.encode(kind, forKey: .kind) + } } extension GraphQLObjectType : KeySubscriptable { @@ -432,19 +463,19 @@ func defineInterfaces( return [] } - if !hasTypeOf { - for interface in interfaces { - guard interface.resolveType != nil else { - throw GraphQLError( - message: - "Interface Type \(interface.name) does not provide a \"resolveType\" " + - "function and implementing Type \(name) does not provide a " + - "\"isTypeOf\" function. There is no way to resolve this implementing " + - "type during execution." - ) - } - } - } +// if !hasTypeOf { +// for interface in interfaces { +// guard interface.resolveType != nil else { +// throw GraphQLError( +// message: +// "Interface Type \(interface.name) does not provide a \"resolveType\" " + +// "function and implementing Type \(name) does not provide a " + +// "\"isTypeOf\" function. There is no way to resolve this implementing " + +// "type during execution." +// ) +// } +// } +// } return interfaces } @@ -753,25 +784,44 @@ public final class GraphQLInterfaceType { public let name: String public let description: String? public let resolveType: GraphQLTypeResolve? - public let fields: GraphQLFieldDefinitionMap + private let fieldsThunk: () -> GraphQLFieldMap + public lazy var fields: GraphQLFieldDefinitionMap = { + try! defineFieldMap( + name: name, + fields: fieldsThunk() + ) + }() public let interfaces: [GraphQLInterfaceType] public let kind: TypeKind = .interface + + public convenience init( + name: String, + description: String? = nil, + interfaces: [GraphQLInterfaceType] = [], + fields: GraphQLFieldMap, + resolveType: GraphQLTypeResolve? = nil + ) throws { + try self.init( + name: name, + description: description, + interfaces: interfaces, + fields: { fields }, + resolveType: resolveType + ) + } public init( name: String, description: String? = nil, interfaces: [GraphQLInterfaceType] = [], - fields: GraphQLFieldMap, + fields: @escaping () -> GraphQLFieldMap, resolveType: GraphQLTypeResolve? = nil ) throws { try assertValid(name: name) self.name = name self.description = description - self.fields = try defineFieldMap( - name: name, - fields: fields - ) + self.fieldsThunk = fields self.interfaces = interfaces self.resolveType = resolveType @@ -791,6 +841,14 @@ extension GraphQLInterfaceType : Encodable { case fields case kind } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(fields, forKey: .fields) + try container.encode(kind, forKey: .kind) + } } extension GraphQLInterfaceType : KeySubscriptable { @@ -855,26 +913,43 @@ public final class GraphQLUnionType { public let name: String public let description: String? public let resolveType: GraphQLTypeResolve? - public let types: [GraphQLObjectType] + private let typesThunk: () -> [GraphQLObjectType] + public lazy var types = { + try! defineTypes( + name: name, + hasResolve: resolveType != nil, + types: typesThunk() + ) + }() public let possibleTypeNames: [String: Bool] public let kind: TypeKind = .union + + public convenience init( + name: String, + description: String? = nil, + resolveType: GraphQLTypeResolve? = nil, + types: [GraphQLObjectType] + ) throws { + try self.init( + name: name, + description: description, + resolveType: resolveType, + types: { types } + ) + } public init( name: String, description: String? = nil, resolveType: GraphQLTypeResolve? = nil, - types: [GraphQLObjectType] + types: @escaping () -> [GraphQLObjectType] ) throws { try assertValid(name: name) self.name = name self.description = description self.resolveType = resolveType - self.types = try defineTypes( - name: name, - hasResolve: resolveType != nil, - types: types - ) + self.typesThunk = types self.possibleTypeNames = [:] } @@ -887,6 +962,14 @@ extension GraphQLUnionType : Encodable { case types case kind } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(types, forKey: .types) + try container.encode(kind, forKey: .kind) + } } extension GraphQLUnionType : KeySubscriptable { @@ -1024,7 +1107,7 @@ public final class GraphQLEnumType { } public func parseLiteral(valueAST: Value) -> Map { - if let enumValue = valueAST as? EnumValue { + if case .enumValue(let enumValue) = valueAST { return nameLookup[enumValue.value]?.value ?? .null } diff --git a/Sources/GraphQL/Type/Directives.swift b/Sources/GraphQL/Type/Directives.swift index ef6e5722..7370e6ad 100644 --- a/Sources/GraphQL/Type/Directives.swift +++ b/Sources/GraphQL/Type/Directives.swift @@ -1,4 +1,4 @@ -public enum DirectiveLocation : String, Encodable { +public enum DirectiveLocation : String, Codable { // Operations case query = "QUERY" case mutation = "MUTATION" diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 1ccb4e97..cf14c4e7 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -476,3 +476,7 @@ let TypeNameMetaFieldDef = GraphQLFieldDefinition( eventLoopGroup.next().makeSucceededFuture(info.parentType.name) } ) + +let introspectionTypes: [GraphQLNamedType] = [ + __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue, __EnumValue, __TypeKind +] diff --git a/Sources/GraphQL/Type/Scalars.swift b/Sources/GraphQL/Type/Scalars.swift index c04c9baa..caad5071 100644 --- a/Sources/GraphQL/Type/Scalars.swift +++ b/Sources/GraphQL/Type/Scalars.swift @@ -6,7 +6,7 @@ public let GraphQLInt = try! GraphQLScalarType( serialize: { try map(from: $0) } , parseValue: { try .int($0.intValue(converting: true)) }, parseLiteral: { ast in - if let ast = ast as? IntValue, let int = Int(ast.value) { + if case .intValue(let ast) = ast, let int = Int(ast.value) { return .int(int) } @@ -23,11 +23,11 @@ public let GraphQLFloat = try! GraphQLScalarType( serialize: { try map(from: $0) } , parseValue: { try .double($0.doubleValue(converting: true)) }, parseLiteral: { ast in - if let ast = ast as? FloatValue, let double = Double(ast.value) { + if case .floatValue(let ast) = ast, let double = Double(ast.value) { return .double(double) } - if let ast = ast as? IntValue, let double = Double(ast.value) { + if case .intValue(let ast) = ast, let double = Double(ast.value) { return .double(double) } @@ -44,7 +44,7 @@ public let GraphQLString = try! GraphQLScalarType( serialize: { try map(from: $0) } , parseValue: { try .string($0.stringValue(converting: true)) }, parseLiteral: { ast in - if let ast = ast as? StringValue { + if case .stringValue(let ast) = ast { return .string(ast.value) } @@ -58,7 +58,7 @@ public let GraphQLBoolean = try! GraphQLScalarType( serialize: { try map(from: $0) } , parseValue: { try .bool($0.boolValue(converting: true)) }, parseLiteral: { ast in - if let ast = ast as? BooleanValue { + if case .booleanValue(let ast) = ast { return .bool(ast.value) } @@ -77,14 +77,18 @@ public let GraphQLID = try! GraphQLScalarType( serialize: { try map(from: $0) }, parseValue: { try .string($0.stringValue(converting: true)) }, parseLiteral: { ast in - if let ast = ast as? StringValue { + if case .stringValue(let ast) = ast { return .string(ast.value) } - if let ast = ast as? IntValue { + if case .intValue(let ast) = ast { return .string(ast.value) } return .null } ) + +public let specifiedScalarTypes = [ + GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID +] diff --git a/Sources/GraphQL/Utilities/ASTFromValue.swift b/Sources/GraphQL/Utilities/ASTFromValue.swift index daafe086..735cee64 100644 --- a/Sources/GraphQL/Utilities/ASTFromValue.swift +++ b/Sources/GraphQL/Utilities/ASTFromValue.swift @@ -44,7 +44,7 @@ func astFromValue( } } - return ListValue(values: valuesASTs) + return .listValue(ListValue(values: valuesASTs)) } return try astFromValue(value: value, type: itemType) @@ -69,7 +69,7 @@ func astFromValue( } } - return ObjectValue(fields: fieldASTs) + return .objectValue(ObjectValue(fields: fieldASTs)) } guard let leafType = type as? GraphQLLeafType else { @@ -90,11 +90,11 @@ func astFromValue( if case let .number(number) = serialized { switch number.storageType { case .bool: - return BooleanValue(value: number.boolValue) + return .booleanValue(BooleanValue(value: number.boolValue)) case .int: - return IntValue(value: String(number.intValue)) + return .intValue(IntValue(value: String(number.intValue))) case .double: - return FloatValue(value: String(number.doubleValue)) + return .floatValue(FloatValue(value: String(number.doubleValue))) case .unknown: break } @@ -103,12 +103,12 @@ func astFromValue( if case let .string(string) = serialized { // Enum types use Enum literals. if type is GraphQLEnumType { - return EnumValue(value: string) + return .enumValue(EnumValue(value: string)) } // ID types can use Int literals. if type == GraphQLID && Int(string) != nil { - return IntValue(value: string) + return .intValue(IntValue(value: string)) } // Use JSON stringify, which uses the same string encoding as GraphQL, @@ -119,7 +119,7 @@ func astFromValue( let data = try JSONEncoder().encode(Wrapper(map: serialized)) let string = String(data: data, encoding: .utf8)! - return StringValue(value: String(string.dropFirst(8).dropLast(2))) + return .stringValue(StringValue(value: String(string.dropFirst(8).dropLast(2)))) } throw GraphQLError(message: "Cannot convert value to AST: \(serialized)") diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift new file mode 100644 index 00000000..c53e9cc7 --- /dev/null +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -0,0 +1,162 @@ +enum BuildClientSchemaError: Error { + case invalid(String) +} +public func buildClientSchema(introspection: IntrospectionQuery) throws -> GraphQLSchema { + let schemaIntrospection = introspection.__schema + var typeMap = [String: GraphQLNamedType]() + typeMap = try schemaIntrospection.types.reduce(into: [String: GraphQLNamedType]()) { + $0[$1.x.name] = try buildType($1.x) + } + + + // Include standard types only if they are used + for stdType in specifiedScalarTypes + introspectionTypes { + if (typeMap[stdType.name] != nil) { + typeMap[stdType.name] = stdType + } + } + + func getNamedType(name: String) throws -> GraphQLNamedType { + guard let type = typeMap[name] else { + throw BuildClientSchemaError.invalid("Couldn't find type named \(name)") + } + return type + } + + func buildImplementationsList(interfaces: [IntrospectionTypeRef]) throws -> [GraphQLInterfaceType] { + try interfaces.map { + switch $0 { + case .named(_, let name): + return try getInterfaceType(name: name) + default: + throw BuildClientSchemaError.invalid("Expected named type ref") + } + } + } + + func getInterfaceType(name: String) throws -> GraphQLInterfaceType { + guard let type = try getNamedType(name: name) as? GraphQLInterfaceType else { + throw BuildClientSchemaError.invalid("Expected interface type") + } + return type + } + + func getType(_ typeRef: IntrospectionTypeRef) throws -> GraphQLType { + switch typeRef { + case .list(let ofType): + return GraphQLList(try getType(ofType)) + case .nonNull(let ofType): + guard let type = try getType(ofType) as? GraphQLNullableType else { + throw BuildClientSchemaError.invalid("Expected nullable type") + } + return GraphQLNonNull(type) + case .named(_, let name): + return try getNamedType(name: name) + } + } + + func buildFieldDefMap(fields: [IntrospectionField]) throws -> GraphQLFieldMap { + try fields.reduce( + into: GraphQLFieldMap()) { + guard let type = try getType($1.type) as? GraphQLOutputType else { + throw BuildClientSchemaError.invalid("Introspection must provide output type for fields") + } + $0[$1.name] = GraphQLField( + type: type, + description: $1.description, + deprecationReason: $1.deprecationReason, + args: try buildInputValueDefMap(args: $1.args) + ) + } + } + + func buildInputValueDefMap(args: [IntrospectionInputValue]) throws -> GraphQLArgumentConfigMap { + return try args.reduce(into: GraphQLArgumentConfigMap()) { + $0[$1.name] = try buildInputValue(inputValue: $1) + } + } + + func buildInputValue(inputValue: IntrospectionInputValue) throws -> GraphQLArgument { + guard let type = try getType(inputValue.type) as? GraphQLInputType else { + throw BuildClientSchemaError.invalid("Introspection must provide input type for arguments") + } + let defaultValue = try inputValue.defaultValue.map { + try valueFromAST(valueAST: parseValue(source: $0), type: type) + } + return GraphQLArgument(type: type, description: inputValue.description, defaultValue: defaultValue) + } + + func buildType(_ type: IntrospectionType) throws -> GraphQLNamedType { + switch type { + case let type as IntrospectionScalarType: + return try GraphQLScalarType( + name: type.name, + description: type.description, + serialize: { try map(from: $0) } + ) + case let type as IntrospectionObjectType: + return try GraphQLObjectType( + name: type.name, + description: type.description, + fields: { try! buildFieldDefMap(fields: type.fields ?? []) }, + interfaces: { try! buildImplementationsList(interfaces: type.interfaces ?? []) } + ) + case let type as IntrospectionInterfaceType: + return try GraphQLInterfaceType( + name: type.name, + description: type.description, + interfaces: buildImplementationsList(interfaces: type.interfaces ?? []), + fields: { try! buildFieldDefMap(fields: type.fields ?? []) }, + resolveType: nil + ) + case let type as IntrospectionUnionType: + return try GraphQLUnionType( + name: type.name, + description: type.description, + types: { try! type.possibleTypes.map(getObjectType) } + ) + case let type as IntrospectionEnumType: + return try GraphQLEnumType( + name: type.name, + description: type.description, + values: type.enumValues.reduce(into: GraphQLEnumValueMap()) { + $0[$1.name] = GraphQLEnumValue( + value: Map.null, + description: $1.description, + deprecationReason: $1.deprecationReason + ) + } + ) +// case .scalar: +// return GraphQLScalarType(name: type["name"].string!, description: type["description"].string) +// case .object: +// return GraphQLObjectType(name: type["name"].string!, description: type["description"].string, fields: buildFieldDefMap(type: type), interfaces: buildImplementationsList(type)) + default: + fatalError() + } + } + + func getObjectType(_ type: IntrospectionTypeRef) throws -> GraphQLObjectType { + guard case .named(_, let name) = type else { + throw BuildClientSchemaError.invalid("Expected name ref") + } + return try getObjectType(name: name) + } + + func getObjectType(name: String) throws -> GraphQLObjectType { + guard let type = try getNamedType(name: name) as? GraphQLObjectType else { + throw BuildClientSchemaError.invalid("Expected object type") + } + return type + } + +// let directives = [] + + return try GraphQLSchema( + query: try getObjectType(name: schemaIntrospection.queryType.name), + mutation: nil, + subscription: nil, + types: Array(typeMap.values), + directives: [] + ) +} diff --git a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift new file mode 100644 index 00000000..d5ef6790 --- /dev/null +++ b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift @@ -0,0 +1,382 @@ +func getIntrospectionQuery( + descriptions: Bool = true, + specifiedByUrl: Bool = false, + directiveIsRepeatable: Bool = false, + schemaDescription: Bool = false, + inputValueDeprecation: Bool = false +) -> String { + + let descriptions = descriptions ? "description" : "" + let specifiedByUrl = specifiedByUrl ? "specifiedByURL" : "" + let directiveIsRepeatable = directiveIsRepeatable ? "isRepeatable" : "" + let schemaDescription = schemaDescription ? descriptions : "" + + func inputDeprecation(_ str: String) -> String { + return inputValueDeprecation ? str : "" + } + + return """ + query IntrospectionQuery { + __schema { + \(schemaDescription) + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + \(descriptions) + \(directiveIsRepeatable) + locations + args\(inputDeprecation("(includeDeprecated: true)")) { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + \(descriptions) + \(specifiedByUrl) + fields(includeDeprecated: true) { + name + \(descriptions) + args\(inputDeprecation("(includeDeprecated: true)")) { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields\(inputDeprecation("(includeDeprecated: true)")) { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + \(descriptions) + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + \(descriptions) + type { ...TypeRef } + defaultValue + \(inputDeprecation("isDeprecated")) + \(inputDeprecation("deprecationReason")) + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } + """ +} + +public struct IntrospectionQuery: Codable { + let __schema: IntrospectionSchema +} + +struct IntrospectionSchema: Codable { + let description: String? + let queryType: IntrospectionKindlessNamedTypeRef + let mutationType: IntrospectionKindlessNamedTypeRef? + let subscriptionType: IntrospectionKindlessNamedTypeRef? + let types: [AnyIntrospectionType] +// let directives: [IntrospectionDirective] + + func encode(to encoder: Encoder) throws { + try types.encode(to: encoder) + } +} + +protocol IntrospectionType: Codable { + static var kind: TypeKind2 { get } + var name: String { get } +} + +enum IntrospectionTypeCodingKeys: String, CodingKey { + case kind +} +//enum IntrospectionType: Codable { +// case scalar(name: String, description: String?, specifiedByURL: String?) +// +// enum IntrospectionScalarTypeCodingKeys: CodingKey { +// case kind, name, description, specifiedByURL +// } +// func encode(to encoder: Encoder) throws { +// switch self { +// case .scalar(let name, let description, let specifiedByURL): +// var container = encoder.container(keyedBy: IntrospectionScalarTypeCodingKeys.self) +// try container.encode(TypeKind2.scalar, forKey: .kind) +// try container.encode(name, forKey: .name) +// try container.encode(description, forKey: .description) +// try container.encode(specifiedByURL, forKey: .specifiedByURL) +// } +// } +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: IntrospectionTypeCodingKeys.self) +// switch try container.decode(TypeKind2.self, forKey: .kind) { +// case .scalar: +// let container = try decoder.container(keyedBy: IntrospectionScalarTypeCodingKeys.self) +// self = .scalar( +// name: try container.decode(String.self, forKey: .name), +// description: try container.decode(String.self, forKey: .description), +// specifiedByURL: try container.decode(String.self, forKey: .description) +// ) +// } +// } +//} + +enum TypeKind2 : String, Codable { + case scalar = "SCALAR" + case object = "OBJECT" + case interface = "INTERFACE" + case union = "UNION" + case `enum` = "ENUM" + case inputObject = "INPUT_OBJECT" +// case list = "LIST" +// case nonNull = "NON_NULL" +} + +struct AnyIntrospectionType: Codable { + let x: IntrospectionType + func encode(to encoder: Encoder) throws { + try x.encode(to: encoder) + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: IntrospectionTypeCodingKeys.self) + switch try container.decode(TypeKind2.self, forKey: .kind) { + case .scalar: + x = try IntrospectionScalarType(from: decoder) + case .object: + x = try IntrospectionObjectType(from: decoder) + case .interface: + x = try IntrospectionInterfaceType(from: decoder) + case .union: + x = try IntrospectionUnionType(from: decoder) + case .enum: + x = try IntrospectionEnumType(from: decoder) + case .inputObject: + x = try IntrospectionInputObjectType(from: decoder) + } + } +} + +//protocol IntrospectionOutputType: Codable {} +//extension IntrospectionScalarType: IntrospectionOutputType {} +// +//protocol IntrospectionInputType: Codable {} +//extension IntrospectionScalarType: IntrospectionInputType {} +//extension IntrospectionEnumType: IntrospectionInputType {} +//extension IntrospectionInputObjectType: IntrospectionInputType {} +// +struct IntrospectionScalarType: IntrospectionType { + static let kind = TypeKind2.scalar + let name: String + let description: String? + let specifiedByURL: String? +} + +struct IntrospectionObjectType: IntrospectionType { + static let kind = TypeKind2.object + let name: String + let description: String? + let fields: [IntrospectionField]? + let interfaces: [IntrospectionTypeRef]? +} + +struct IntrospectionInterfaceType: IntrospectionType { + static let kind = TypeKind2.interface + let name: String + let description: String? + let fields: [IntrospectionField]? + let interfaces: [IntrospectionTypeRef]? + let possibleTypes: [IntrospectionTypeRef] +} + +struct IntrospectionUnionType: IntrospectionType { + static let kind = TypeKind2.union + let name: String + let description: String? + let possibleTypes: [IntrospectionTypeRef] +} + +struct IntrospectionEnumType: IntrospectionType { + static let kind = TypeKind2.enum + let name: String + let description: String? + let enumValues: [IntrospectionEnumValue] +} + +struct IntrospectionInputObjectType: IntrospectionType { + static let kind = TypeKind2.inputObject + let name: String + let description: String? + let inputFields: [IntrospectionInputValue] +} +// +//protocol IntrospectionTypeRef: Codable {} +//extension IntrospectionNamedTypeRef: IntrospectionTypeRef {} +// +//protocol IntrospectionOutputTypeRef: Codable {} +//extension IntrospectionNamedTypeRef: IntrospectionOutputTypeRef where T: IntrospectionOutputType {} +//extension IntrospectionListTypeRef: IntrospectionOutputTypeRef where T: IntrospectionOutputType {} + +//struct AnyIntrospectionOutputTypeRef: Codable { +// let typeRef: IntrospectionOutputTypeRef +// func encode(to encoder: Encoder) throws { +// try typeRef.encode(to: encoder) +// } +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: IntrospectionTypeCodingKeys.self) +// switch try container.decode(TypeKind2.self, forKey: .kind) { +// case .list: +// typeRef = try IntrospectionListTypeRef<<#T: IntrospectionTypeRef#>>(from: decoder) +// default: +// fatalError() +// } +// } +//} + +//protocol IntrospectionInputTypeRef: Codable {} +//extension IntrospectionNamedTypeRef: IntrospectionInputTypeRef where T: IntrospectionInputType {} +// +//struct IntrospectionListTypeRef: IntrospectionType { +// static var kind: TypeKind2 { TypeKind2.list } +// let ofType: T +//} + +//struct IntrospectionNamedTypeRef: Codable { +// var kind: TypeKind2 = T.kind +// let name: String +//} + +indirect enum IntrospectionTypeRef: Codable { + case named(kind: TypeKind2, name: String) + case list(ofType: IntrospectionTypeRef) + case nonNull(ofType: IntrospectionTypeRef) + + enum NamedTypeRefCodingKeys: CodingKey { + case kind, name + } + enum ListTypeRefCodingKeys: CodingKey { + case kind, ofType + } + + enum TypeRefKind: Codable { + case list, nonNull, named(TypeKind2) + } + + func encode(to encoder: Encoder) throws { + switch self { + case .named(let kind, let name): + var container = encoder.container(keyedBy: NamedTypeRefCodingKeys.self) + try container.encode(kind, forKey: .kind) + try container.encode(name, forKey: .name) + case .list(let ofType): + var container = encoder.container(keyedBy: ListTypeRefCodingKeys.self) + try container.encode("LIST", forKey: .kind) + try container.encode(ofType, forKey: .ofType) + case .nonNull(let ofType): + var container = encoder.container(keyedBy: ListTypeRefCodingKeys.self) + try container.encode("NON_NULL", forKey: .kind) + try container.encode(ofType, forKey: .ofType) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: NamedTypeRefCodingKeys.self) + if let kind = try? container.decode(TypeKind2.self, forKey: .kind) { + self = .named(kind: kind, name: try container.decode(String.self, forKey: .name)) + } else { + let container = try decoder.container(keyedBy: ListTypeRefCodingKeys.self) + let kind = try container.decode(String.self, forKey: .kind) + switch kind { + case "LIST": + self = .list(ofType: try container.decode(IntrospectionTypeRef.self, forKey: .ofType)) + case "NON_NULL": + self = .nonNull(ofType: try container.decode(IntrospectionTypeRef.self, forKey: .ofType)) + default: + fatalError() + } + } + } +} + +struct IntrospectionKindlessNamedTypeRef: Codable { + let name: String +} + +struct IntrospectionField: Codable { + let name: String + let description: String? + let args: [IntrospectionInputValue] + let type: IntrospectionTypeRef + let isDeprecated: Bool + let deprecationReason: String? +} + +struct IntrospectionInputValue: Codable { + let name: String + let description: String? + let type: IntrospectionTypeRef + let defaultValue: String? + let isDeprecated: Bool? + let deprecationReason: String? +} + +struct IntrospectionEnumValue: Codable { + let name: String + let description: String? + let isDeprecated: Bool + let deprecationReason: String? +} + +struct IntrospectionDirective: Codable { + let name: String + let description: String? + let isRepeatable: Bool? + let locations: [DirectiveLocation] + let args: [IntrospectionInputValue] +} diff --git a/Sources/GraphQL/Utilities/TypeFromAST.swift b/Sources/GraphQL/Utilities/TypeFromAST.swift index 39501ad8..e1626c68 100644 --- a/Sources/GraphQL/Utilities/TypeFromAST.swift +++ b/Sources/GraphQL/Utilities/TypeFromAST.swift @@ -1,19 +1,15 @@ func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> GraphQLType? { - if let listType = inputTypeAST as? ListType { + switch inputTypeAST { + case let .listType(listType): if let innerType = typeFromAST(schema: schema, inputTypeAST: listType.type) { return GraphQLList(innerType) } - } - - if let nonNullType = inputTypeAST as? NonNullType { + case let .nonNullType(nonNullType): if let innerType = typeFromAST(schema: schema, inputTypeAST: nonNullType.type) { return GraphQLNonNull(innerType as! GraphQLNullableType) } + case let .namedType(namedType): + return schema.getType(name: namedType.name.value) } - - guard let namedType = inputTypeAST as? NamedType else { - return nil - } - - return schema.getType(name: namedType.name.value) + return nil } diff --git a/Sources/GraphQL/Utilities/TypeInfo.swift b/Sources/GraphQL/Utilities/TypeInfo.swift index 1483e69c..a1bb28a6 100644 --- a/Sources/GraphQL/Utilities/TypeInfo.swift +++ b/Sources/GraphQL/Utilities/TypeInfo.swift @@ -3,7 +3,7 @@ * of the current field and type definitions at any point in a GraphQL document * AST during a recursive descent by calling `enter(node: node)` and `leave(node: node)`. */ -final class TypeInfo { +public final class TypeInfo { let schema: GraphQLSchema; var typeStack: [GraphQLOutputType?] var parentTypeStack: [GraphQLCompositeType?] @@ -12,7 +12,7 @@ final class TypeInfo { var directive: GraphQLDirective? var argument: GraphQLArgumentDefinition? - init(schema: GraphQLSchema) { + public init(schema: GraphQLSchema) { self.schema = schema self.typeStack = [] self.parentTypeStack = [] @@ -22,35 +22,35 @@ final class TypeInfo { self.argument = nil } - var type: GraphQLOutputType? { + public var type: GraphQLOutputType? { if !typeStack.isEmpty { return typeStack[typeStack.count - 1] } return nil } - var parentType: GraphQLCompositeType? { + public var parentType: GraphQLCompositeType? { if !parentTypeStack.isEmpty { return parentTypeStack[parentTypeStack.count - 1] } return nil } - var inputType: GraphQLInputType? { + public var inputType: GraphQLInputType? { if !inputTypeStack.isEmpty { return inputTypeStack[inputTypeStack.count - 1] } return nil } - var fieldDef: GraphQLFieldDefinition? { + public var fieldDef: GraphQLFieldDefinition? { if !fieldDefStack.isEmpty { return fieldDefStack[fieldDefStack.count - 1] } return nil } - func enter(node: Node) { + public func enter(node: Node) { switch node { case is SelectionSet: let namedType = getNamedType(type: type) @@ -91,11 +91,11 @@ final class TypeInfo { case let node as InlineFragment: let typeConditionAST = node.typeCondition - let outputType = typeConditionAST != nil ? typeFromAST(schema: schema, inputTypeAST: typeConditionAST!) : self.type + let outputType = typeConditionAST != nil ? typeFromAST(schema: schema, inputTypeAST: .namedType(typeConditionAST!)) : self.type typeStack.append(outputType as? GraphQLOutputType) case let node as FragmentDefinition: - let outputType = typeFromAST(schema: schema, inputTypeAST: node.typeCondition) + let outputType = typeFromAST(schema: schema, inputTypeAST: .namedType(node.typeCondition)) typeStack.append(outputType as? GraphQLOutputType) case let node as VariableDefinition: @@ -137,7 +137,7 @@ final class TypeInfo { } } - func leave(node: Node) { + public func leave(node: Node) { switch node { case is SelectionSet: _ = parentTypeStack.popLast() @@ -149,7 +149,7 @@ final class TypeInfo { case is Directive: directive = nil - case is OperationDefinition, is InlineFragment, is FragmentDefinition: + case is Definition, is InlineFragment: _ = typeStack.popLast() case is VariableDefinition: @@ -173,7 +173,7 @@ final class TypeInfo { * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. */ -func getFieldDef(schema: GraphQLSchema, parentType: GraphQLType, fieldAST: Field) -> GraphQLFieldDefinition? { +public func getFieldDef(schema: GraphQLSchema, parentType: GraphQLType, fieldAST: Field) -> GraphQLFieldDefinition? { let name = fieldAST.name.value if let parentType = parentType as? GraphQLNamedType { diff --git a/Sources/GraphQL/Utilities/ValueFromAST.swift b/Sources/GraphQL/Utilities/ValueFromAST.swift index 6e92be72..e37a0d7c 100644 --- a/Sources/GraphQL/Utilities/ValueFromAST.swift +++ b/Sources/GraphQL/Utilities/ValueFromAST.swift @@ -28,7 +28,7 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M return try valueFromAST(valueAST: valueAST, type: nonNullType, variables: variables) } - if let variable = valueAST as? Variable { + if case .variable(let variable) = valueAST { let variableName = variable.name.value // if (!variables || !variables.hasOwnProperty(variableName)) { @@ -49,7 +49,7 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M throw GraphQLError(message: "Input list must wrap an input type") } - if let listValue = valueAST as? ListValue { + if case .listValue(let listValue) = valueAST { let values = try listValue.values.map { item in try valueFromAST( valueAST: item, @@ -71,7 +71,7 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M } if let objectType = type as? GraphQLInputObjectType { - guard let objectValue = valueAST as? ObjectValue else { + guard case .objectValue(let objectValue) = valueAST else { throw GraphQLError(message: "Input object must be object type") } diff --git a/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift b/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift index 0cb46a24..4a71de5c 100644 --- a/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift +++ b/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift @@ -23,48 +23,45 @@ func undefinedFieldMessage( * A GraphQL document is only valid if all fields selected are defined by the * parent type, or are an allowed meta field such as __typename. */ -func FieldsOnCorrectTypeRule(context: ValidationContext) -> Visitor { - return Visitor( - enter: { node, key, parent, path, ancestors in - if let node = node as? Field { - if let type = context.parentType { - let fieldDef = context.fieldDef - if fieldDef == nil { - // This field doesn't exist, lets look for suggestions. - let schema = context.schema - let fieldName = node.name.value - - // First determine if there are any suggested types to condition on. - let suggestedTypeNames = getSuggestedTypeNames( - schema: schema, - type: type, - fieldName: fieldName - ) - - // If there are no suggested types, then perhaps this was a typo? - let suggestedFieldNames = !suggestedTypeNames.isEmpty ? [] : getSuggestedFieldNames( - schema: schema, - type: type, - fieldName: fieldName - ) - - // Report an error, including helpful suggestions. - context.report(error: GraphQLError( - message: undefinedFieldMessage( - fieldName: fieldName, - type: type.name, - suggestedTypeNames: suggestedTypeNames, - suggestedFieldNames: suggestedFieldNames - ), - nodes: [node] - )) - } - } - } - +struct FieldsOnCorrectTypeRule: ValidationRule { + let context: ValidationContext + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + guard let type = context.parentType else { return .continue } - ) + guard context.fieldDef == nil else { + return .continue + } + // This field doesn't exist, lets look for suggestions. + let schema = context.schema + let fieldName = field.name.value + + // First determine if there are any suggested types to condition on. + let suggestedTypeNames = getSuggestedTypeNames( + schema: schema, + type: type, + fieldName: fieldName + ) + + // If there are no suggested types, then perhaps this was a typo? + let suggestedFieldNames = !suggestedTypeNames.isEmpty ? [] : getSuggestedFieldNames( + schema: schema, + type: type, + fieldName: fieldName + ) + + // Report an error, including helpful suggestions. + context.report(error: GraphQLError( + message: undefinedFieldMessage( + fieldName: fieldName, + type: type.name, + suggestedTypeNames: suggestedTypeNames, + suggestedFieldNames: suggestedFieldNames + ), + nodes: [field] + )) + return .continue + } } /** diff --git a/Sources/GraphQL/Validation/Rules/KnownArgumentNamesRule.swift b/Sources/GraphQL/Validation/Rules/KnownArgumentNamesRule.swift index e9b15925..f8ab9ff7 100644 --- a/Sources/GraphQL/Validation/Rules/KnownArgumentNamesRule.swift +++ b/Sources/GraphQL/Validation/Rules/KnownArgumentNamesRule.swift @@ -16,27 +16,25 @@ import Foundation return message } - func KnownArgumentNamesRule(context: ValidationContext) -> Visitor { - return Visitor( - enter: { node, key, parent, path, ancestors in - if let node = node as? Argument, context.argument == nil, let field = context.fieldDef, let type = context.parentType { - let argumentName = node.name.value - let suggestedArgumentNames = getSuggestedArgumentNames(schema: context.schema, field: field, argumentName: argumentName) - - context.report(error: GraphQLError( - message: undefinedArgumentMessage( - fieldName: field.name, - type: type.name, - argumentName: argumentName, - suggestedArgumentNames: suggestedArgumentNames - ), - nodes: [node] - )) - } - - return .continue +struct KnownArgumentNamesRule: ValidationRule { + let context: ValidationContext + func enter(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if context.argument == nil, let field = context.fieldDef, let type = context.parentType { + let argumentName = argument.name.value + let suggestedArgumentNames = getSuggestedArgumentNames(schema: context.schema, field: field, argumentName: argumentName) + + context.report(error: GraphQLError( + message: undefinedArgumentMessage( + fieldName: field.name, + type: type.name, + argumentName: argumentName, + suggestedArgumentNames: suggestedArgumentNames + ), + nodes: [argument] + )) } - ) + return .continue + } } func getSuggestedArgumentNames( diff --git a/Sources/GraphQL/Validation/Rules/NoUnusedVariablesRule.swift b/Sources/GraphQL/Validation/Rules/NoUnusedVariablesRule.swift index 7daa3bd5..4bc06424 100644 --- a/Sources/GraphQL/Validation/Rules/NoUnusedVariablesRule.swift +++ b/Sources/GraphQL/Validation/Rules/NoUnusedVariablesRule.swift @@ -4,51 +4,37 @@ * A GraphQL operation is only valid if all variables defined by an operation * are used, either directly or within a spread fragment. */ -func NoUnusedVariablesRule(context: ValidationContext) -> Visitor { - var variableDefs: [VariableDefinition] = [] - - return Visitor( - enter: { node, _, _, _, _ in - if node is OperationDefinition { - variableDefs = [] - return .continue - } +class NoUnusedVariablesRule: ValidationRule { + private var variableDefs: [VariableDefinition] = [] + let context: ValidationContext + required init(context: ValidationContext) { self.context = context } + + func enter(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + variableDefs = [] + return .continue + } + + func enter(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + variableDefs.append(variableDefinition) + return .continue + } + + func leave(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + let usages = Set(context.getRecursiveVariableUsages(operation: operationDefinition).map { $0.node.name }) + + for variableDef in variableDefs where !usages.contains(variableDef.variable.name) { + let variableName = variableDef.variable.name.value - if let def = node as? VariableDefinition { - variableDefs.append(def) - return .continue - } - - return .continue - }, - leave: { node, _, _, _, _ -> VisitResult in - guard let operation = node as? OperationDefinition else { - return .continue - } - - var variableNameUsed: [String: Bool] = [:] - let usages = context.getRecursiveVariableUsages(operation: operation) - - for usage in usages { - variableNameUsed[usage.node.name.value] = true - } - - for variableDef in variableDefs { - let variableName = variableDef.variable.name.value - - if variableNameUsed[variableName] != true { - context.report( - error: GraphQLError( - message: operation.name.map { - "Variable \"$\(variableName)\" is never used in operation \"\($0.value)\"." - } ?? "Variable \"$\(variableName)\" is never used.", - nodes: [variableDef] - ) - ) - } - } - - return .continue + context.report( + error: GraphQLError( + message: operationDefinition.name.map { + "Variable \"$\(variableName)\" is never used in operation \"\($0.value)\"." + } ?? "Variable \"$\(variableName)\" is never used.", + nodes: [variableDef] + ) + ) } - ) + + return .continue + } } diff --git a/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift b/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift index 86b46241..12150026 100644 --- a/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift +++ b/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift @@ -5,66 +5,65 @@ * be true: if there is a non-empty intersection of the possible parent types, * and possible types which pass the type condition. */ -func PossibleFragmentSpreadsRule(context: ValidationContext) -> Visitor { - return Visitor( - enter: { node, key, parent, path, ancestors in - if let node = node as? InlineFragment { - guard - let fragType = context.type as? GraphQLCompositeType, - let parentType = context.parentType - else { - return .continue - } - - let isThereOverlap = doTypesOverlap( - schema: context.schema, - typeA: fragType, - typeB: parentType - ) - - guard !isThereOverlap else { - return .continue - } - - context.report( - error: GraphQLError( - message: "Fragment cannot be spread here as objects of type \"\(parentType)\" can never be of type \"\(fragType)\".", - nodes: [node] - ) - ) - } - - if let node = node as? FragmentSpread { - let fragName = node.name.value - - guard - let fragType = getFragmentType(context: context, name: fragName), - let parentType = context.parentType - else { - return .continue - } - - let isThereOverlap = doTypesOverlap( - schema: context.schema, - typeA: fragType, - typeB: parentType - ) - - guard !isThereOverlap else { - return .continue - } - - context.report( - error: GraphQLError( - message: "Fragment \"\(fragName)\" cannot be spread here as objects of type \"\(parentType)\" can never be of type \"\(fragType)\".", - nodes: [node] - ) - ) - } - +struct PossibleFragmentSpreadsRule: ValidationRule { + let context: ValidationContext + + func enter(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + guard + let fragType = context.type as? GraphQLCompositeType, + let parentType = context.parentType + else { return .continue } - ) + + let isThereOverlap = doTypesOverlap( + schema: context.schema, + typeA: fragType, + typeB: parentType + ) + + guard !isThereOverlap else { + return .continue + } + + context.report( + error: GraphQLError( + message: "Fragment cannot be spread here as objects of type \"\(parentType)\" can never be of type \"\(fragType)\".", + nodes: [inlineFragment] + ) + ) + return .continue + } + + func enter(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + + let fragName = fragmentSpread.name.value + + guard + let fragType = getFragmentType(context: context, name: fragName), + let parentType = context.parentType + else { + return .continue + } + + let isThereOverlap = doTypesOverlap( + schema: context.schema, + typeA: fragType, + typeB: parentType + ) + + guard !isThereOverlap else { + return .continue + } + + context.report( + error: GraphQLError( + message: "Fragment \"\(fragName)\" cannot be spread here as objects of type \"\(parentType)\" can never be of type \"\(fragType)\".", + nodes: [fragmentSpread] + ) + ) + return .continue + } } func getFragmentType( @@ -74,7 +73,7 @@ func getFragmentType( if let fragment = context.getFragment(name: name) { let type = typeFromAST( schema: context.schema, - inputTypeAST: fragment.typeCondition + inputTypeAST: .namedType(fragment.typeCondition) ) if let type = type as? GraphQLCompositeType { diff --git a/Sources/GraphQL/Validation/Rules/ProvidedNonNullArgumentsRule.swift b/Sources/GraphQL/Validation/Rules/ProvidedNonNullArgumentsRule.swift index c9335e6a..f7f1f769 100644 --- a/Sources/GraphQL/Validation/Rules/ProvidedNonNullArgumentsRule.swift +++ b/Sources/GraphQL/Validation/Rules/ProvidedNonNullArgumentsRule.swift @@ -9,33 +9,32 @@ func missingArgumentsMessage( return "Field \"\(fieldName)\" on type \"\(type)\" is missing required arguments \(arguments)." } - func ProvidedNonNullArgumentsRule(context: ValidationContext) -> Visitor { - return Visitor( - leave: { node, key, parent, path, ancestors in - if let node = node as? Field, let field = context.fieldDef, let type = context.parentType { - let requiredArguments = Set( - field - .args - .filter { $0.type is GraphQLNonNull && $0.defaultValue == nil } - .map { $0.name } - ) - - let providedArguments = Set(node.arguments.map { $0.name.value }) - - let missingArguments = requiredArguments.subtracting(providedArguments) - if !missingArguments.isEmpty { - context.report(error: GraphQLError( - message: missingArgumentsMessage( - fieldName: field.name, - type: type.name, - missingArguments: Array(missingArguments) - ), - nodes: [node] - )) - } +struct ProvidedNonNullArgumentsRule: ValidationRule { + let context: ValidationContext + func leave(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if let fieldDef = context.fieldDef, let type = context.parentType { + let requiredArguments = Set( + fieldDef + .args + .filter { $0.type is GraphQLNonNull && $0.defaultValue == nil } + .map { $0.name } + ) + + let providedArguments = Set(field.arguments.map { $0.name.value }) + + let missingArguments = requiredArguments.subtracting(providedArguments) + if !missingArguments.isEmpty { + context.report(error: GraphQLError( + message: missingArgumentsMessage( + fieldName: fieldDef.name, + type: type.name, + missingArguments: Array(missingArguments) + ), + nodes: [field] + )) } - - return .continue } - ) + + return .continue + } } diff --git a/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift b/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift index 2379d7a8..27feb673 100644 --- a/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift +++ b/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift @@ -14,30 +14,26 @@ func requiredSubselectionMessage(fieldName: String, type: GraphQLType) -> String * A GraphQL document is valid only if all leaf fields (fields without * sub selections) are of scalar or enum types. */ -func ScalarLeafsRule(context: ValidationContext) -> Visitor { - return Visitor( - enter: { node, key, parent, path, ancestors in - if let node = node as? Field { - if let type = context.type { - if isLeafType(type: getNamedType(type: type)) { - if let selectionSet = node.selectionSet { - let error = GraphQLError( - message: noSubselectionAllowedMessage(fieldName: node.name.value, type: type), - nodes: [selectionSet] - ) - context.report(error: error) - } - } else if node.selectionSet == nil { - let error = GraphQLError( - message: requiredSubselectionMessage(fieldName: node.name.value, type: type), - nodes: [node] - ) - context.report(error: error) - } +struct ScalarLeafsRule: ValidationRule { + let context: ValidationContext + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if let type = context.type { + if isLeafType(type: getNamedType(type: type)) { + if let selectionSet = field.selectionSet { + let error = GraphQLError( + message: noSubselectionAllowedMessage(fieldName: field.name.value, type: type), + nodes: [selectionSet] + ) + context.report(error: error) } + } else if field.selectionSet == nil { + let error = GraphQLError( + message: requiredSubselectionMessage(fieldName: field.name.value, type: type), + nodes: [field] + ) + context.report(error: error) } - - return .continue } - ) + return .continue + } } diff --git a/Sources/GraphQL/Validation/SpecifiedRules.swift b/Sources/GraphQL/Validation/SpecifiedRules.swift index 74a521c9..6d28c360 100644 --- a/Sources/GraphQL/Validation/SpecifiedRules.swift +++ b/Sources/GraphQL/Validation/SpecifiedRules.swift @@ -1,29 +1,29 @@ /** * This set includes all validation rules defined by the GraphQL spec. */ -let specifiedRules: [(ValidationContext) -> Visitor] = [ -// UniqueOperationNames, -// LoneAnonymousOperation, -// KnownTypeNames, -// FragmentsOnCompositeTypes, -// VariablesAreInputTypes, - ScalarLeafsRule, - FieldsOnCorrectTypeRule, -// UniqueFragmentNames, -// KnownFragmentNames, -// NoUnusedFragments, - PossibleFragmentSpreadsRule, -// NoFragmentCycles, -// UniqueVariableNames, -// NoUndefinedVariables, - NoUnusedVariablesRule, -// KnownDirectives, - KnownArgumentNamesRule, -// UniqueArgumentNames, -// ArgumentsOfCorrectType, - ProvidedNonNullArgumentsRule, -// DefaultValuesOfCorrectType, -// VariablesInAllowedPosition, -// OverlappingFieldsCanBeMerged, -// UniqueInputFieldNames, +let specifiedRules: [ValidationRule.Type] = [ + // UniqueOperationNames, + // LoneAnonymousOperation, + // KnownTypeNames, + // FragmentsOnCompositeTypes, + // VariablesAreInputTypes, + ScalarLeafsRule.self, + FieldsOnCorrectTypeRule.self, + // UniqueFragmentNames, + // KnownFragmentNames, + // NoUnusedFragments, + PossibleFragmentSpreadsRule.self, + // NoFragmentCycles, + // UniqueVariableNames, + // NoUndefinedVariables, + NoUnusedVariablesRule.self, + // KnownDirectives, + KnownArgumentNamesRule.self, + // UniqueArgumentNames, + // ArgumentsOfCorrectType, + ProvidedNonNullArgumentsRule.self, + // DefaultValuesOfCorrectType, + // VariablesInAllowedPosition, + // OverlappingFieldsCanBeMerged, + // UniqueInputFieldNames, ] diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index baa342fa..f4c8b5fe 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -1,21 +1,3 @@ -/// Implements the "Validation" section of the spec. -/// -/// Validation runs synchronously, returning an array of encountered errors, or -/// an empty array if no errors were encountered and the document is valid. -/// -/// - Parameters: -/// - instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages. -/// - schema: The GraphQL type system to use when validating and executing a query. -/// - ast: A GraphQL document representing the requested operation. -/// - Returns: zero or more errors -public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - ast: Document -) -> [GraphQLError] { - return validate(instrumentation: instrumentation, schema: schema, ast: ast, rules: []) -} - /** * Implements the "Validation" section of the spec. * @@ -28,21 +10,44 @@ public func validate( * Each validation rules is a function which returns a visitor * (see the language/visitor API). Visitor methods are expected to return * GraphQLErrors, or Arrays of GraphQLErrors when invalid. + * + * - Parameters: + * - instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages. + * - schema: The GraphQL type system to use when validating and executing a query. + * - ast: A GraphQL document representing the requested operation. + */ + +public func validate( + instrumentation: Instrumentation = NoOpInstrumentation, + schema: GraphQLSchema, + ast: Document +) -> [GraphQLError] { + validate(instrumentation: instrumentation, schema: schema, ast: ast, rules: specifiedRules) +} + +/** + * An internal version of `validate` that lets you specify custom validation rules. + * + * - Parameters: + * - rules: A list of specific validation rules. If not provided, the default list of rules defined by the GraphQL specification will be used. */ func validate( instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, ast: Document, - rules: [(ValidationContext) -> Visitor] + rules: [ValidationRule.Type] ) -> [GraphQLError] { let started = instrumentation.now let typeInfo = TypeInfo(schema: schema) - let rules = rules.isEmpty ? specifiedRules : rules let errors = visit(usingRules: rules, schema: schema, typeInfo: typeInfo, documentAST: ast) instrumentation.queryValidation(processId: processId(), threadId: threadId(), started: started, finished: instrumentation.now, schema: schema, document: ast, errors: errors) return errors } +protocol ValidationRule: Visitor { + init(context: ValidationContext) +} + /** * This uses a specialized visitor which runs multiple visitors in parallel, * while maintaining the visitor skip and break API. @@ -50,53 +55,28 @@ func validate( * @internal */ func visit( - usingRules rules: [(ValidationContext) -> Visitor], + usingRules rules: [ValidationRule.Type], schema: GraphQLSchema, typeInfo: TypeInfo, documentAST: Document ) -> [GraphQLError] { let context = ValidationContext(schema: schema, ast: documentAST, typeInfo: typeInfo) - let visitors = rules.map({ rule in rule(context) }) + let visitors = rules.map({ rule in rule.init(context: context) }) // Visit the whole document with each instance of all provided rules. - visit(root: documentAST, visitor: visitWithTypeInfo(typeInfo: typeInfo, visitor: visitInParallel(visitors: visitors))) + visit(root: documentAST, visitor: VisitorWithTypeInfo( + visitor: ParallelVisitor(visitors: visitors), + typeInfo: typeInfo + )) return context.errors } -enum HasSelectionSet { - case operation(OperationDefinition) - case fragment(FragmentDefinition) - - var node: Node { - switch self { - case .operation(let operation): - return operation - case .fragment(let fragment): - return fragment - } - } +protocol HasSelectionSet: Node { + var selectionSet: SelectionSet { get } } -extension HasSelectionSet : Hashable { - func hash(into hasher: inout Hasher) { - switch self { - case .operation(let operation): - return hasher.combine(operation.hashValue) - case .fragment(let fragment): - return hasher.combine(fragment.hashValue) - } - } +extension OperationDefinition: HasSelectionSet { } +extension FragmentDefinition: HasSelectionSet { } - static func == (lhs: HasSelectionSet, rhs: HasSelectionSet) -> Bool { - switch (lhs, rhs) { - case (.operation(let l), .operation(let r)): - return l == r - case (.fragment(let l), .fragment(let r)): - return l == r - default: - return false - } - } -} typealias VariableUsage = (node: Variable, type: GraphQLInputType?) @@ -111,10 +91,11 @@ final class ValidationContext { let typeInfo: TypeInfo var errors: [GraphQLError] var fragments: [String: FragmentDefinition] - var fragmentSpreads: [SelectionSet: [FragmentSpread]] - var recursivelyReferencedFragments: [OperationDefinition: [FragmentDefinition]] - var variableUsages: [HasSelectionSet: [VariableUsage]] - var recursiveVariableUsages: [OperationDefinition: [VariableUsage]] + // TODO: memoise all these caches +// var fragmentSpreads: [SelectionSet: [FragmentSpread]] +// var recursivelyReferencedFragments: [OperationDefinition: [FragmentDefinition]] +// var variableUsages: [HasSelectionSet: [VariableUsage]] +// var recursiveVariableUsages: [OperationDefinition: [VariableUsage]] init(schema: GraphQLSchema, ast: Document, typeInfo: TypeInfo) { self.schema = schema @@ -122,10 +103,10 @@ final class ValidationContext { self.typeInfo = typeInfo self.errors = [] self.fragments = [:] - self.fragmentSpreads = [:] - self.recursivelyReferencedFragments = [:] - self.variableUsages = [:] - self.recursiveVariableUsages = [:] +// self.fragmentSpreads = [:] +// self.recursivelyReferencedFragments = [:] +// self.variableUsages = [:] +// self.recursiveVariableUsages = [:] } func report(error: GraphQLError) { @@ -139,7 +120,7 @@ final class ValidationContext { fragments = ast.definitions.reduce([:]) { frags, statement in var frags = frags - if let statement = statement as? FragmentDefinition { + if case let .executableDefinition(.fragment(statement)) = statement { frags[statement.name.value] = statement } @@ -153,105 +134,77 @@ final class ValidationContext { } func getFragmentSpreads(node: SelectionSet) -> [FragmentSpread] { - var spreads = fragmentSpreads[node] - - if spreads == nil { - spreads = [] - var setsToVisit: [SelectionSet] = [node] - - while let set = setsToVisit.popLast() { - for selection in set.selections { - if let selection = selection as? FragmentSpread { - spreads!.append(selection) - } - - if let selection = selection as? InlineFragment { - setsToVisit.append(selection.selectionSet) - } - - if let selection = selection as? Field, let selectionSet = selection.selectionSet { + var spreads: [FragmentSpread] = [] + var setsToVisit: [SelectionSet] = [node] + + while let set = setsToVisit.popLast() { + for selection in set.selections { + switch selection { + case let .fragmentSpread(fragmentSpread): + spreads.append(fragmentSpread) + case let .inlineFragment(inlineFragment): + setsToVisit.append(inlineFragment.selectionSet) + case let .field(field): + if let selectionSet = field.selectionSet { setsToVisit.append(selectionSet) } } } - - fragmentSpreads[node] = spreads } - - return spreads! + return spreads } func getRecursivelyReferencedFragments(operation: OperationDefinition) -> [FragmentDefinition] { - var fragments = recursivelyReferencedFragments[operation] - - if fragments == nil { - fragments = [] - var collectedNames: [String: Bool] = [:] - var nodesToVisit: [SelectionSet] = [operation.selectionSet] - - while let node = nodesToVisit.popLast() { - let spreads = getFragmentSpreads(node: node) - - for spread in spreads { - let fragName = spread.name.value - if collectedNames[fragName] != true { - collectedNames[fragName] = true - if let fragment = getFragment(name: fragName) { - fragments!.append(fragment) - nodesToVisit.append(fragment.selectionSet) - } + var fragments: [FragmentDefinition] = [] + var collectedNames: [String: Bool] = [:] + var nodesToVisit: [SelectionSet] = [operation.selectionSet] + + while let node = nodesToVisit.popLast() { + let spreads = getFragmentSpreads(node: node) + + for spread in spreads { + let fragName = spread.name.value + if collectedNames[fragName] != true { + collectedNames[fragName] = true + if let fragment = getFragment(name: fragName) { + fragments.append(fragment) + nodesToVisit.append(fragment.selectionSet) } } } - - recursivelyReferencedFragments[operation] = fragments } - - return fragments! - } - - func getVariableUsages(node: HasSelectionSet) -> [VariableUsage] { - var usages = variableUsages[node] - - if usages == nil { - var newUsages: [VariableUsage] = [] - let typeInfo = TypeInfo(schema: schema) - - visit(root: node.node, visitor: visitWithTypeInfo(typeInfo: typeInfo, visitor: Visitor(enter: { node, _, _, _, _ in - if node is VariableDefinition { - return .skip - } - - if let variable = node as? Variable { - newUsages.append(VariableUsage(node: variable, type: typeInfo.inputType)) - } - - return .continue - }))) - - usages = newUsages - variableUsages[node] = usages + return fragments + } + + class VariableUsageFinder: Visitor { + var newUsages: [VariableUsage] = [] + let typeInfo: TypeInfo + init(typeInfo: TypeInfo) { self.typeInfo = typeInfo } + func enter(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + .skip + } + func enter(variable: Variable, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + newUsages.append(VariableUsage(node: variable, type: typeInfo.inputType)) + return .continue } + } - return usages! + func getVariableUsages(node: T) -> [VariableUsage] { + let typeInfo = TypeInfo(schema: schema) + let visitor = VariableUsageFinder(typeInfo: typeInfo) + visit(root: node, visitor: VisitorWithTypeInfo(visitor: visitor, typeInfo: typeInfo)) + return visitor.newUsages } func getRecursiveVariableUsages(operation: OperationDefinition) -> [VariableUsage] { - var usages = recursiveVariableUsages[operation] - - if usages == nil { - usages = getVariableUsages(node: .operation(operation)) - let fragments = getRecursivelyReferencedFragments(operation: operation) - - for fragment in fragments { - let newUsages = getVariableUsages(node: .fragment(fragment)) - usages!.append(contentsOf: newUsages) - } - - recursiveVariableUsages[operation] = usages - } + var usages = getVariableUsages(node: operation) + let fragments = getRecursivelyReferencedFragments(operation: operation) - return usages! + for fragment in fragments { + let newUsages = getVariableUsages(node: fragment) + usages.append(contentsOf: newUsages) + } + return usages } var type: GraphQLOutputType? { diff --git a/Tests/GraphQLTests/LanguageTests/ParserTests.swift b/Tests/GraphQLTests/LanguageTests/ParserTests.swift index 49260e86..c327f6e3 100644 --- a/Tests/GraphQLTests/LanguageTests/ParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/ParserTests.swift @@ -1,459 +1,459 @@ -import XCTest -@testable import GraphQL - -class ParserTests : XCTestCase { - func testErrorMessages() throws { - var source: String - - XCTAssertThrowsError(try parse(source: "{")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssertEqual(error.message, - "Syntax Error GraphQL (1:2) Expected Name, found \n\n" + - " 1: {\n" + - " ^\n" - ) - - XCTAssertEqual(error.positions, [1]) - XCTAssertEqual(error.locations[0].line, 1) - XCTAssertEqual(error.locations[0].column, 2) - } - - XCTAssertThrowsError(try parse(source: "{ ...MissingOn }\nfragment MissingOn Type\n")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (2:20) Expected \"on\", found Name \"Type\"" - )) - } - - XCTAssertThrowsError(try parse(source: "{ field: {} }")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:10) Expected Name, found {" - )) - } - - XCTAssertThrowsError(try parse(source: "notanoperation Foo { field }")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:1) Unexpected Name \"notanoperation\"" - )) - } - - XCTAssertThrowsError(try parse(source: "...")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:1) Unexpected ..." - )) - } - - XCTAssertThrowsError(try parse(source: Source(body: "query", name: "MyQuery.graphql"))) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error MyQuery.graphql (1:6) Expected {, found " - )) - } - - source = "query Foo($x: Complex = { a: { b: [ $var ] } }) { field }" - - XCTAssertThrowsError(try parse(source: source)) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:37) Unexpected $" - )) - } - - XCTAssertThrowsError(try parse(source: "fragment on on on { on }")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:10) Unexpected Name \"on\"" - )) - } - - XCTAssertThrowsError(try parse(source: "{ ...on }")) { error in - guard let error = error as? GraphQLError else { - return XCTFail() - } - - XCTAssert(error.message.contains( - "Syntax Error GraphQL (1:9) Expected Name, found }" - )) - } - - } - - func testVariableInlineValues() throws { - _ = try parse(source: "{ field(complex: { a: { b: [ $var ] } }) }") - } - - func testFieldWithArguments() throws { - let query = """ - { - stringArgField(stringArg: "Hello World") - intArgField(intArg: 1) - floatArgField(floatArg: 3.14) - falseArgField(boolArg: false) - trueArgField(boolArg: true) - nullArgField(value: null) - enumArgField(enumArg: VALUE) - multipleArgs(arg1: 1, arg2: false, arg3: THIRD) - } - """ - - let expected = Document( - definitions: [ - OperationDefinition( - operation: .query, - selectionSet: SelectionSet( - selections: [ - Field( - name: Name(value: "stringArgField"), - arguments: [ - Argument( - name: Name(value: "stringArg"), - value: StringValue(value: "Hello World", block: false) - ) - ] - ), - Field( - name: Name(value: "intArgField"), - arguments: [ - Argument( - name: Name(value: "intArg"), - value: IntValue(value: "1") - ) - ] - ), - Field( - name: Name(value: "floatArgField"), - arguments: [ - Argument( - name: Name(value: "floatArg"), - value: FloatValue(value: "3.14") - ) - ] - ), - Field( - name: Name(value: "falseArgField"), - arguments: [ - Argument( - name: Name(value: "boolArg"), - value: BooleanValue(value: false) - ) - ] - ), - Field( - name: Name(value: "trueArgField"), - arguments: [ - Argument( - name: Name(value: "boolArg"), - value: BooleanValue(value: true) - ) - ] - ), - Field( - name: Name(value: "nullArgField"), - arguments: [ - Argument( - name: Name(value: "value"), - value: NullValue() - ) - ] - ), - Field( - name: Name(value: "enumArgField"), - arguments: [ - Argument( - name: Name(value: "enumArg"), - value: EnumValue(value: "VALUE") - ) - ] - ), - Field( - name: Name(value: "multipleArgs"), - arguments: [ - Argument( - name: Name(value: "arg1"), - value: IntValue(value: "1") - ), - Argument( - name: Name(value: "arg2"), - value: BooleanValue(value: false) - ), - Argument( - name: Name(value: "arg3"), - value: EnumValue(value: "THIRD") - ), - ] - ), - ] - ) - ) - ] - ) - - let document = try parse(source: query) - XCTAssert(document == expected) - } - -// it('parses multi-byte characters', async () => { -// // Note: \u0A0A could be naively interpretted as two line-feed chars. -// expect( -// parse(` -// # This comment has a \u0A0A multi-byte character. -// { field(arg: "Has a \u0A0A multi-byte character.") } -// `) -// ).to.containSubset({ -// definitions: [ { -// selectionSet: { -// selections: [ { -// arguments: [ { -// value: { -// kind: Kind.STRING, -// value: 'Has a \u0A0A multi-byte character.' -// } -// } ] -// } ] +//import XCTest +//@testable import GraphQL +// +//class ParserTests : XCTestCase { +// func testErrorMessages() throws { +// var source: String +// +// XCTAssertThrowsError(try parse(source: "{")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssertEqual(error.message, +// "Syntax Error GraphQL (1:2) Expected Name, found \n\n" + +// " 1: {\n" + +// " ^\n" +// ) +// +// XCTAssertEqual(error.positions, [1]) +// XCTAssertEqual(error.locations[0].line, 1) +// XCTAssertEqual(error.locations[0].column, 2) // } -// } ] -// }); -// }); - - func testKitchenSink() throws { -// let path = "/Users/paulofaria/Development/Zewo/GraphQL/Tests/GraphQLTests/LanguageTests/kitchen-sink.graphql" -// let kitchenSink = try NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) -// _ = try parse(source: kitchenSink as String) - } - - func testNonKeywordAsName() throws { - let nonKeywords = [ - "on", - "fragment", - "query", - "mutation", - "subscription", - "true", - "false" - ] - - for nonKeyword in nonKeywords { - var fragmentName = nonKeyword - // You can't define or reference a fragment named `on`. - if nonKeyword == "on" { - fragmentName = "a" - } - - _ = try parse(source: "query \(nonKeyword) {" + - "... \(fragmentName)" + - "... on \(nonKeyword) { field }" + - "}" + - "fragment \(fragmentName) on Type {" + - "\(nonKeyword)(\(nonKeyword): $\(nonKeyword)) @\(nonKeyword)(\(nonKeyword): \(nonKeyword))" + - "}" - ) - } - } - - func testAnonymousMutationOperation() throws { - _ = try parse(source: "mutation {" + - " mutationField" + - "}" - ) - } - - func testAnonymousSubscriptionOperation() throws { - _ = try parse(source: "subscription {" + - " subscriptionField" + - "}" - ) - } - - func testNamedMutationOperation() throws { - _ = try parse(source: "mutation Foo {" + - " mutationField" + - "}" - ) - } - - func testNamedSubscriptionOperation() throws { - _ = try parse(source: "subscription Foo {" + - " subscriptionField" + - "}" - ) - } - - func testCreateAST() throws { - let query = "{" + - " node(id: 4) {" + - " id," + - " name" + - " }" + - "}" - - let expected = Document( - definitions: [ - OperationDefinition( - operation: .query, - selectionSet: SelectionSet( - selections: [ - Field( - name: Name(value: "node"), - arguments: [ - Argument( - name: Name(value: "id"), - value: IntValue(value: "4") - ) - ], - selectionSet: SelectionSet( - selections: [ - Field(name: Name(value: "id")), - Field(name: Name(value: "name")) - ] - ) - ) - ] - ) - ) - ] - ) - - XCTAssert(try parse(source: query) == expected) - } - - func testNoLocation() throws { - let result = try parse(source: "{ id }", noLocation: true) - XCTAssertNil(result.loc) - } - - func testLocationSource() throws { - let source = Source(body: "{ id }") - let result = try parse(source: source) - XCTAssertEqual(result.loc?.source, source) - } - - func testLocationTokens() throws { - let source = Source(body: "{ id }") - let result = try parse(source: source) - XCTAssertEqual(result.loc?.startToken.kind, .sof) - XCTAssertEqual(result.loc?.endToken.kind, .eof) - } - - func testParseValue() throws { - let source = "[123 \"abc\"]" - - let expected: Value = ListValue( - values: [ - IntValue(value: "123"), - StringValue(value: "abc", block: false) - ] - ) - - XCTAssert(try parseValue(source: source) == expected) - } - - func testParseType() throws { - var source: String - var expected: Type - - source = "String" - - expected = NamedType( - name: Name(value: "String") - ) - - XCTAssert(try parseType(source: source) == expected) - - source = "MyType" - - expected = NamedType( - name: Name(value: "MyType") - ) - - XCTAssert(try parseType(source: source) == expected) - - source = "[MyType]" - - expected = ListType( - type: NamedType( - name: Name(value: "MyType") - ) - ) - - XCTAssert(try parseType(source: source) == expected) - - source = "MyType!" - - expected = NonNullType( - type: NamedType( - name: Name(value: "MyType") - ) - ) - - XCTAssert(try parseType(source: source) == expected) - - source = "[MyType!]" - - expected = ListType( - type: NonNullType( - type: NamedType( - name: Name(value: "MyType") - ) - ) - ) - - XCTAssert(try parseType(source: source) == expected) - } - - func testParseDirective() throws { - let source = #""" - directive @restricted( - """The reason for this restriction""" - reason: String = null - ) on FIELD_DEFINITION - """# - - let expected = Document(definitions: [ - DirectiveDefinition( - description: nil, - name: Name(value: "restricted"), - arguments: [ - InputValueDefinition( - description: StringValue(value: "The reason for this restriction", block: true), - name: Name(value: "reason"), - type: NamedType(name: Name(value: "String")), - defaultValue: NullValue() - ) - ], - locations: [ - Name(value: "FIELD_DEFINITION") - ] - ) - ]) - - let document = try parse(source: source) - XCTAssert(document == expected) - } -} +// +// XCTAssertThrowsError(try parse(source: "{ ...MissingOn }\nfragment MissingOn Type\n")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (2:20) Expected \"on\", found Name \"Type\"" +// )) +// } +// +// XCTAssertThrowsError(try parse(source: "{ field: {} }")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:10) Expected Name, found {" +// )) +// } +// +// XCTAssertThrowsError(try parse(source: "notanoperation Foo { field }")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:1) Unexpected Name \"notanoperation\"" +// )) +// } +// +// XCTAssertThrowsError(try parse(source: "...")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:1) Unexpected ..." +// )) +// } +// +// XCTAssertThrowsError(try parse(source: Source(body: "query", name: "MyQuery.graphql"))) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error MyQuery.graphql (1:6) Expected {, found " +// )) +// } +// +// source = "query Foo($x: Complex = { a: { b: [ $var ] } }) { field }" +// +// XCTAssertThrowsError(try parse(source: source)) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:37) Unexpected $" +// )) +// } +// +// XCTAssertThrowsError(try parse(source: "fragment on on on { on }")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:10) Unexpected Name \"on\"" +// )) +// } +// +// XCTAssertThrowsError(try parse(source: "{ ...on }")) { error in +// guard let error = error as? GraphQLError else { +// return XCTFail() +// } +// +// XCTAssert(error.message.contains( +// "Syntax Error GraphQL (1:9) Expected Name, found }" +// )) +// } +// +// } +// +// func testVariableInlineValues() throws { +// _ = try parse(source: "{ field(complex: { a: { b: [ $var ] } }) }") +// } +// +// func testFieldWithArguments() throws { +// let query = """ +// { +// stringArgField(stringArg: "Hello World") +// intArgField(intArg: 1) +// floatArgField(floatArg: 3.14) +// falseArgField(boolArg: false) +// trueArgField(boolArg: true) +// nullArgField(value: null) +// enumArgField(enumArg: VALUE) +// multipleArgs(arg1: 1, arg2: false, arg3: THIRD) +// } +// """ +// +// let expected = Document( +// definitions: [ +// OperationDefinition( +// operation: .query, +// selectionSet: SelectionSet( +// selections: [ +// Field( +// name: Name(value: "stringArgField"), +// arguments: [ +// Argument( +// name: Name(value: "stringArg"), +// value: StringValue(value: "Hello World", block: false) +// ) +// ] +// ), +// Field( +// name: Name(value: "intArgField"), +// arguments: [ +// Argument( +// name: Name(value: "intArg"), +// value: IntValue(value: "1") +// ) +// ] +// ), +// Field( +// name: Name(value: "floatArgField"), +// arguments: [ +// Argument( +// name: Name(value: "floatArg"), +// value: FloatValue(value: "3.14") +// ) +// ] +// ), +// Field( +// name: Name(value: "falseArgField"), +// arguments: [ +// Argument( +// name: Name(value: "boolArg"), +// value: BooleanValue(value: false) +// ) +// ] +// ), +// Field( +// name: Name(value: "trueArgField"), +// arguments: [ +// Argument( +// name: Name(value: "boolArg"), +// value: BooleanValue(value: true) +// ) +// ] +// ), +// Field( +// name: Name(value: "nullArgField"), +// arguments: [ +// Argument( +// name: Name(value: "value"), +// value: NullValue() +// ) +// ] +// ), +// Field( +// name: Name(value: "enumArgField"), +// arguments: [ +// Argument( +// name: Name(value: "enumArg"), +// value: EnumValue(value: "VALUE") +// ) +// ] +// ), +// Field( +// name: Name(value: "multipleArgs"), +// arguments: [ +// Argument( +// name: Name(value: "arg1"), +// value: IntValue(value: "1") +// ), +// Argument( +// name: Name(value: "arg2"), +// value: BooleanValue(value: false) +// ), +// Argument( +// name: Name(value: "arg3"), +// value: EnumValue(value: "THIRD") +// ), +// ] +// ), +// ] +// ) +// ) +// ] +// ) +// +// let document = try parse(source: query) +// XCTAssert(document == expected) +// } +// +//// it('parses multi-byte characters', async () => { +//// // Note: \u0A0A could be naively interpretted as two line-feed chars. +//// expect( +//// parse(` +//// # This comment has a \u0A0A multi-byte character. +//// { field(arg: "Has a \u0A0A multi-byte character.") } +//// `) +//// ).to.containSubset({ +//// definitions: [ { +//// selectionSet: { +//// selections: [ { +//// arguments: [ { +//// value: { +//// kind: Kind.STRING, +//// value: 'Has a \u0A0A multi-byte character.' +//// } +//// } ] +//// } ] +//// } +//// } ] +//// }); +//// }); +// +// func testKitchenSink() throws { +//// let path = "/Users/paulofaria/Development/Zewo/GraphQL/Tests/GraphQLTests/LanguageTests/kitchen-sink.graphql" +//// let kitchenSink = try NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) +//// _ = try parse(source: kitchenSink as String) +// } +// +// func testNonKeywordAsName() throws { +// let nonKeywords = [ +// "on", +// "fragment", +// "query", +// "mutation", +// "subscription", +// "true", +// "false" +// ] +// +// for nonKeyword in nonKeywords { +// var fragmentName = nonKeyword +// // You can't define or reference a fragment named `on`. +// if nonKeyword == "on" { +// fragmentName = "a" +// } +// +// _ = try parse(source: "query \(nonKeyword) {" + +// "... \(fragmentName)" + +// "... on \(nonKeyword) { field }" + +// "}" + +// "fragment \(fragmentName) on Type {" + +// "\(nonKeyword)(\(nonKeyword): $\(nonKeyword)) @\(nonKeyword)(\(nonKeyword): \(nonKeyword))" + +// "}" +// ) +// } +// } +// +// func testAnonymousMutationOperation() throws { +// _ = try parse(source: "mutation {" + +// " mutationField" + +// "}" +// ) +// } +// +// func testAnonymousSubscriptionOperation() throws { +// _ = try parse(source: "subscription {" + +// " subscriptionField" + +// "}" +// ) +// } +// +// func testNamedMutationOperation() throws { +// _ = try parse(source: "mutation Foo {" + +// " mutationField" + +// "}" +// ) +// } +// +// func testNamedSubscriptionOperation() throws { +// _ = try parse(source: "subscription Foo {" + +// " subscriptionField" + +// "}" +// ) +// } +// +// func testCreateAST() throws { +// let query = "{" + +// " node(id: 4) {" + +// " id," + +// " name" + +// " }" + +// "}" +// +// let expected = Document( +// definitions: [ +// OperationDefinition( +// operation: .query, +// selectionSet: SelectionSet( +// selections: [ +// Field( +// name: Name(value: "node"), +// arguments: [ +// Argument( +// name: Name(value: "id"), +// value: IntValue(value: "4") +// ) +// ], +// selectionSet: SelectionSet( +// selections: [ +// Field(name: Name(value: "id")), +// Field(name: Name(value: "name")) +// ] +// ) +// ) +// ] +// ) +// ) +// ] +// ) +// +// XCTAssert(try parse(source: query) == expected) +// } +// +// func testNoLocation() throws { +// let result = try parse(source: "{ id }", noLocation: true) +// XCTAssertNil(result.loc) +// } +// +// func testLocationSource() throws { +// let source = Source(body: "{ id }") +// let result = try parse(source: source) +// XCTAssertEqual(result.loc?.source, source) +// } +// +// func testLocationTokens() throws { +// let source = Source(body: "{ id }") +// let result = try parse(source: source) +// XCTAssertEqual(result.loc?.startToken.kind, .sof) +// XCTAssertEqual(result.loc?.endToken.kind, .eof) +// } +// +// func testParseValue() throws { +// let source = "[123 \"abc\"]" +// +// let expected: Value = ListValue( +// values: [ +// IntValue(value: "123"), +// StringValue(value: "abc", block: false) +// ] +// ) +// +// XCTAssert(try parseValue(source: source) == expected) +// } +// +// func testParseType() throws { +// var source: String +// var expected: Type +// +// source = "String" +// +// expected = NamedType( +// name: Name(value: "String") +// ) +// +// XCTAssert(try parseType(source: source) == expected) +// +// source = "MyType" +// +// expected = NamedType( +// name: Name(value: "MyType") +// ) +// +// XCTAssert(try parseType(source: source) == expected) +// +// source = "[MyType]" +// +// expected = ListType( +// type: NamedType( +// name: Name(value: "MyType") +// ) +// ) +// +// XCTAssert(try parseType(source: source) == expected) +// +// source = "MyType!" +// +// expected = NonNullType( +// type: NamedType( +// name: Name(value: "MyType") +// ) +// ) +// +// XCTAssert(try parseType(source: source) == expected) +// +// source = "[MyType!]" +// +// expected = ListType( +// type: NonNullType( +// type: NamedType( +// name: Name(value: "MyType") +// ) +// ) +// ) +// +// XCTAssert(try parseType(source: source) == expected) +// } +// +// func testParseDirective() throws { +// let source = #""" +// directive @restricted( +// """The reason for this restriction""" +// reason: String = null +// ) on FIELD_DEFINITION +// """# +// +// let expected = Document(definitions: [ +// DirectiveDefinition( +// description: nil, +// name: Name(value: "restricted"), +// arguments: [ +// InputValueDefinition( +// description: StringValue(value: "The reason for this restriction", block: true), +// name: Name(value: "reason"), +// type: NamedType(name: Name(value: "String")), +// defaultValue: NullValue() +// ) +// ], +// locations: [ +// Name(value: "FIELD_DEFINITION") +// ] +// ) +// ]) +// +// let document = try parse(source: source) +// XCTAssert(document == expected) +// } +//} diff --git a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift index fa249e4b..2d76c062 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift @@ -1,736 +1,736 @@ -import XCTest -@testable import GraphQL - -func nameNode(_ name: String) -> Name { - return Name(value: name) -} - -func fieldNode(_ name: Name, _ type: Type) -> FieldDefinition { - return FieldDefinition(name: name, type: type) -} - -func fieldNodeWithDescription(_ description: StringValue? = nil, _ name: Name, _ type: Type) -> FieldDefinition { - return FieldDefinition(description: description, name: name, type: type) -} - -func typeNode(_ name: String) -> NamedType { - return NamedType(name: nameNode(name)) -} - -func enumValueNode(_ name: String) -> EnumValueDefinition { - return EnumValueDefinition(name: nameNode(name)) -} - -func enumValueWithDescriptionNode(_ description: StringValue?, _ name: String) -> EnumValueDefinition { - return EnumValueDefinition(description: description, name: nameNode(name)) -} - -func fieldNodeWithArgs(_ name: Name, _ type: Type, _ args: [InputValueDefinition]) -> FieldDefinition { - return FieldDefinition(name: name, arguments: args, type: type) -} - -func inputValueNode(_ name: Name, _ type: Type, _ defaultValue: Value? = nil) -> InputValueDefinition { - return InputValueDefinition(name: name, type: type, defaultValue: defaultValue) -} - -func inputValueWithDescriptionNode(_ description: StringValue?, - _ name: Name, - _ type: Type, - _ defaultValue: Value? = nil) -> InputValueDefinition { - return InputValueDefinition(description: description, name: name, type: type, defaultValue: defaultValue) -} - -func namedTypeNode(_ name: String ) -> NamedType { - return NamedType(name: nameNode(name)) -} - -class SchemaParserTests : XCTestCase { - func testSimpleType() throws { - let source = "type Hello { world: String }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleExtension() throws { - let source = "extend type Hello { world: String }" - - let expected = Document( - definitions: [ - TypeExtensionDefinition( - definition: ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleNonNullType() throws { - let source = "type Hello { world: String! }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - NonNullType( - type: typeNode("String") - ) - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleTypeInheritingInterface() throws { - let source = "type Hello implements World { }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - interfaces: [typeNode("World")] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleTypeInheritingMultipleInterfaces() throws { - let source = "type Hello implements Wo, rld { }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - interfaces: [ - typeNode("Wo"), - typeNode("rld"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSingleValueEnum() throws { - let source = "enum Hello { WORLD }" - - let expected = Document( - definitions: [ - EnumTypeDefinition( - name: nameNode("Hello"), - values: [ - enumValueNode("WORLD"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testDoubleValueEnum() throws { - let source = "enum Hello { WO, RLD }" - - let expected = Document( - definitions: [ - EnumTypeDefinition( - name: nameNode("Hello"), - values: [ - enumValueNode("WO"), - enumValueNode("RLD"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleInterface() throws { - let source = "interface Hello { world: String }" - - let expected = Document( - definitions: [ - InterfaceTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleFieldWithArg() throws { - let source = "type Hello { world(flag: Boolean): String }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithArgs( - nameNode("world"), - typeNode("String"), - [ - inputValueNode( - nameNode("flag"), - typeNode("Boolean") - ) - ] - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleFieldWithArgDefaultValue() throws { - let source = "type Hello { world(flag: Boolean = true): String }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithArgs( - nameNode("world"), - typeNode("String"), - [ - inputValueNode( - nameNode("flag"), - typeNode("Boolean"), - BooleanValue(value: true) - ) - ] - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleFieldWithListArg() throws { - let source = "type Hello { world(things: [String]): String }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithArgs( - nameNode("world"), - typeNode("String"), - [ - inputValueNode( - nameNode("things"), - ListType(type: typeNode("String")) - ) - ] - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleFieldWithTwoArgs() throws { - let source = "type Hello { world(argOne: Boolean, argTwo: Int): String }" - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithArgs( - nameNode("world"), - typeNode("String"), - [ - inputValueNode( - nameNode("argOne"), - typeNode("Boolean") - ), - inputValueNode( - nameNode("argTwo"), - typeNode("Int") - ) - ] - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleUnion() throws { - let source = "union Hello = World" - - let expected = Document( - definitions: [ - UnionTypeDefinition( - name: nameNode("Hello"), - types: [ - typeNode("World"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testUnionTwoTypes() throws { - let source = "union Hello = Wo | Rld" - - let expected = Document( - definitions: [ - UnionTypeDefinition( - name: nameNode("Hello"), - types: [ - typeNode("Wo"), - typeNode("Rld"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testScalar() throws { - let source = "scalar Hello" - - let expected = Document( - definitions: [ - ScalarTypeDefinition( - name: nameNode("Hello") - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleInputObject() throws { - let source = "input Hello { world: String }" - - let expected = Document( - definitions: [ - InputObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - inputValueNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleInputObjectWithArgs() throws { - let source = "input Hello { world(foo: Int): String }" - XCTAssertThrowsError(try parse(source: source)) - } - - func testSimpleSchema() throws { - let source = "schema { query: Hello }" - let expected = SchemaDefinition( - directives: [], - operationTypes: [ - OperationTypeDefinition( - operation: .query, - type: namedTypeNode("Hello") - ) - ] - ) - let result = try parse(source: source) - XCTAssert(result.definitions[0] == expected) - } - - // Description tests - - func testTypeWithDescription() throws { - let source = #""The Hello type" type Hello { world: String }"# - - let expected = ObjectTypeDefinition( - description: StringValue(value: "The Hello type", block: false), - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - - let result = try parse(source: source) - XCTAssertEqual(result.definitions[0] as! ObjectTypeDefinition, expected, "\n\(dump(result.definitions[0]))\n\(dump(expected))\n") - } - - func testTypeWitMultilinehDescription() throws { - let source = #""" - """ - The Hello type. - Multi-line description - """ - type Hello { - world: String - } - """# - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - description: StringValue(value:"The Hello type.\nMulti-line description", block: true), - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testDirectiveDesciption() throws { - let source = #""directive description" directive @Test(a: String = "hello") on FIELD"# - - let expected: Document = Document( - definitions: [ - DirectiveDefinition(loc: nil, - description: StringValue(value: "directive description", block: false), - name: nameNode("Test"), - arguments: [ - inputValueNode( - nameNode("a"), - typeNode("String"), - StringValue(value: "hello", block: false) - ) - ], - locations: [ - nameNode("FIELD") - ]) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testDirectiveMultilineDesciption() throws { - let source = #""" - """ - directive description - """ - directive @Test(a: String = "hello") on FIELD - """# - let expected: Document = Document( - definitions: [ - DirectiveDefinition(loc: nil, - description: StringValue(value: "directive description", block: true), - name: nameNode("Test"), - arguments: [ - inputValueNode( - nameNode("a"), - typeNode("String"), - StringValue(value: "hello", block: false) - ) - ], - locations: [ - nameNode("FIELD") - ]) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleSchemaWithDescription() throws { - let source = #""Hello Schema" schema { query: Hello } "# - - let expected = SchemaDefinition( - description: StringValue(value: "Hello Schema", block: false), - directives: [], - operationTypes: [ - OperationTypeDefinition( - operation: .query, - type: namedTypeNode("Hello") - ) - ] - ) - let result = try parse(source: source) - XCTAssert(result.definitions[0] == expected) - } - - func testScalarWithDescription() throws { - let source = #""Hello Scaler Test" scalar Hello"# - - let expected = Document( - definitions: [ - ScalarTypeDefinition( - description: StringValue(value: "Hello Scaler Test", block: false), - name: nameNode("Hello") - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleInterfaceWithDescription() throws { - let source = #""Hello World Interface" interface Hello { world: String } "# - - let expected = Document( - definitions: [ - InterfaceTypeDefinition( - description: StringValue(value: "Hello World Interface", block: false), - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleUnionWithDescription() throws { - let source = #""Hello World Union!" union Hello = World "# - - let expected = Document( - definitions: [ - UnionTypeDefinition( - description: StringValue(value: "Hello World Union!", block: false), - name: nameNode("Hello"), - types: [ - typeNode("World"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSingleValueEnumDescription() throws { - let source = #""Hello World Enum..." enum Hello { WORLD } "# - - let expected = Document( - definitions: [ - EnumTypeDefinition( - description: StringValue(value: "Hello World Enum...", block: false), - name: nameNode("Hello"), - values: [ - enumValueNode("WORLD"), - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testSimpleInputObjectWithDescription() throws { - let source = #""Hello Input Object" input Hello { world: String }"# - - let expected = Document( - definitions: [ - InputObjectTypeDefinition( - description: StringValue(value: "Hello Input Object", block: false), - name: nameNode("Hello"), - fields: [ - inputValueNode( - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - // Test Fields and values with optional descriptions - - func testSingleValueEnumWithDescription() throws { - let source = """ - enum Hello { - "world description" - WORLD - "Hello there" - HELLO - } - """ - - let expected = Document( - definitions: [ - EnumTypeDefinition( - name: nameNode("Hello"), - values: [ - enumValueWithDescriptionNode(StringValue(value: "world description", block: false), "WORLD"), - enumValueWithDescriptionNode(StringValue(value: "Hello there", block: false), "HELLO") - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testTypeFieldWithDescription() throws { - let source = #"type Hello { "The world field." world: String } "# - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithDescription( - StringValue(value: "The world field.", block: false), - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - - func testTypeFieldWithMultilineDescription() throws { - let source = #""" - type Hello { - """ - The world - field. - """ - world: String - } - """# - - let expected = Document( - definitions: [ - ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNodeWithDescription( - StringValue(value: "The world\nfield.", block: true), - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected, "\(dump(result)) \n\n\(dump(expected))") - } - - - func testSimpleInputObjectFieldWithDescription() throws { - let source = #"input Hello { "World field" world: String }"# - - let expected = Document( - definitions: [ - InputObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - inputValueWithDescriptionNode( - StringValue(value: "World field", block: false), - nameNode("world"), - typeNode("String") - ) - ] - ) - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - -} +//import XCTest +//@testable import GraphQL +// +//func nameNode(_ name: String) -> Name { +// return Name(value: name) +//} +// +//func fieldNode(_ name: Name, _ type: Type) -> FieldDefinition { +// return FieldDefinition(name: name, type: type) +//} +// +//func fieldNodeWithDescription(_ description: StringValue? = nil, _ name: Name, _ type: Type) -> FieldDefinition { +// return FieldDefinition(description: description, name: name, type: type) +//} +// +//func typeNode(_ name: String) -> NamedType { +// return NamedType(name: nameNode(name)) +//} +// +//func enumValueNode(_ name: String) -> EnumValueDefinition { +// return EnumValueDefinition(name: nameNode(name)) +//} +// +//func enumValueWithDescriptionNode(_ description: StringValue?, _ name: String) -> EnumValueDefinition { +// return EnumValueDefinition(description: description, name: nameNode(name)) +//} +// +//func fieldNodeWithArgs(_ name: Name, _ type: Type, _ args: [InputValueDefinition]) -> FieldDefinition { +// return FieldDefinition(name: name, arguments: args, type: type) +//} +// +//func inputValueNode(_ name: Name, _ type: Type, _ defaultValue: Value? = nil) -> InputValueDefinition { +// return InputValueDefinition(name: name, type: type, defaultValue: defaultValue) +//} +// +//func inputValueWithDescriptionNode(_ description: StringValue?, +// _ name: Name, +// _ type: Type, +// _ defaultValue: Value? = nil) -> InputValueDefinition { +// return InputValueDefinition(description: description, name: name, type: type, defaultValue: defaultValue) +//} +// +//func namedTypeNode(_ name: String ) -> NamedType { +// return NamedType(name: nameNode(name)) +//} +// +//class SchemaParserTests : XCTestCase { +// func testSimpleType() throws { +// let source = "type Hello { world: String }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleExtension() throws { +// let source = "extend type Hello { world: String }" +// +// let expected = Document( +// definitions: [ +// TypeExtensionDefinition( +// definition: ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleNonNullType() throws { +// let source = "type Hello { world: String! }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// NonNullType( +// type: typeNode("String") +// ) +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleTypeInheritingInterface() throws { +// let source = "type Hello implements World { }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// interfaces: [typeNode("World")] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleTypeInheritingMultipleInterfaces() throws { +// let source = "type Hello implements Wo, rld { }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// interfaces: [ +// typeNode("Wo"), +// typeNode("rld"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSingleValueEnum() throws { +// let source = "enum Hello { WORLD }" +// +// let expected = Document( +// definitions: [ +// EnumTypeDefinition( +// name: nameNode("Hello"), +// values: [ +// enumValueNode("WORLD"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testDoubleValueEnum() throws { +// let source = "enum Hello { WO, RLD }" +// +// let expected = Document( +// definitions: [ +// EnumTypeDefinition( +// name: nameNode("Hello"), +// values: [ +// enumValueNode("WO"), +// enumValueNode("RLD"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleInterface() throws { +// let source = "interface Hello { world: String }" +// +// let expected = Document( +// definitions: [ +// InterfaceTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleFieldWithArg() throws { +// let source = "type Hello { world(flag: Boolean): String }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithArgs( +// nameNode("world"), +// typeNode("String"), +// [ +// inputValueNode( +// nameNode("flag"), +// typeNode("Boolean") +// ) +// ] +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleFieldWithArgDefaultValue() throws { +// let source = "type Hello { world(flag: Boolean = true): String }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithArgs( +// nameNode("world"), +// typeNode("String"), +// [ +// inputValueNode( +// nameNode("flag"), +// typeNode("Boolean"), +// BooleanValue(value: true) +// ) +// ] +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleFieldWithListArg() throws { +// let source = "type Hello { world(things: [String]): String }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithArgs( +// nameNode("world"), +// typeNode("String"), +// [ +// inputValueNode( +// nameNode("things"), +// ListType(type: typeNode("String")) +// ) +// ] +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleFieldWithTwoArgs() throws { +// let source = "type Hello { world(argOne: Boolean, argTwo: Int): String }" +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithArgs( +// nameNode("world"), +// typeNode("String"), +// [ +// inputValueNode( +// nameNode("argOne"), +// typeNode("Boolean") +// ), +// inputValueNode( +// nameNode("argTwo"), +// typeNode("Int") +// ) +// ] +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleUnion() throws { +// let source = "union Hello = World" +// +// let expected = Document( +// definitions: [ +// UnionTypeDefinition( +// name: nameNode("Hello"), +// types: [ +// typeNode("World"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testUnionTwoTypes() throws { +// let source = "union Hello = Wo | Rld" +// +// let expected = Document( +// definitions: [ +// UnionTypeDefinition( +// name: nameNode("Hello"), +// types: [ +// typeNode("Wo"), +// typeNode("Rld"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testScalar() throws { +// let source = "scalar Hello" +// +// let expected = Document( +// definitions: [ +// ScalarTypeDefinition( +// name: nameNode("Hello") +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleInputObject() throws { +// let source = "input Hello { world: String }" +// +// let expected = Document( +// definitions: [ +// InputObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// inputValueNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleInputObjectWithArgs() throws { +// let source = "input Hello { world(foo: Int): String }" +// XCTAssertThrowsError(try parse(source: source)) +// } +// +// func testSimpleSchema() throws { +// let source = "schema { query: Hello }" +// let expected = SchemaDefinition( +// directives: [], +// operationTypes: [ +// OperationTypeDefinition( +// operation: .query, +// type: namedTypeNode("Hello") +// ) +// ] +// ) +// let result = try parse(source: source) +// XCTAssert(result.definitions[0] == expected) +// } +// +// // Description tests +// +// func testTypeWithDescription() throws { +// let source = #""The Hello type" type Hello { world: String }"# +// +// let expected = ObjectTypeDefinition( +// description: StringValue(value: "The Hello type", block: false), +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssertEqual(result.definitions[0] as! ObjectTypeDefinition, expected, "\n\(dump(result.definitions[0]))\n\(dump(expected))\n") +// } +// +// func testTypeWitMultilinehDescription() throws { +// let source = #""" +// """ +// The Hello type. +// Multi-line description +// """ +// type Hello { +// world: String +// } +// """# +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// description: StringValue(value:"The Hello type.\nMulti-line description", block: true), +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testDirectiveDesciption() throws { +// let source = #""directive description" directive @Test(a: String = "hello") on FIELD"# +// +// let expected: Document = Document( +// definitions: [ +// DirectiveDefinition(loc: nil, +// description: StringValue(value: "directive description", block: false), +// name: nameNode("Test"), +// arguments: [ +// inputValueNode( +// nameNode("a"), +// typeNode("String"), +// StringValue(value: "hello", block: false) +// ) +// ], +// locations: [ +// nameNode("FIELD") +// ]) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testDirectiveMultilineDesciption() throws { +// let source = #""" +// """ +// directive description +// """ +// directive @Test(a: String = "hello") on FIELD +// """# +// let expected: Document = Document( +// definitions: [ +// DirectiveDefinition(loc: nil, +// description: StringValue(value: "directive description", block: true), +// name: nameNode("Test"), +// arguments: [ +// inputValueNode( +// nameNode("a"), +// typeNode("String"), +// StringValue(value: "hello", block: false) +// ) +// ], +// locations: [ +// nameNode("FIELD") +// ]) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleSchemaWithDescription() throws { +// let source = #""Hello Schema" schema { query: Hello } "# +// +// let expected = SchemaDefinition( +// description: StringValue(value: "Hello Schema", block: false), +// directives: [], +// operationTypes: [ +// OperationTypeDefinition( +// operation: .query, +// type: namedTypeNode("Hello") +// ) +// ] +// ) +// let result = try parse(source: source) +// XCTAssert(result.definitions[0] == expected) +// } +// +// func testScalarWithDescription() throws { +// let source = #""Hello Scaler Test" scalar Hello"# +// +// let expected = Document( +// definitions: [ +// ScalarTypeDefinition( +// description: StringValue(value: "Hello Scaler Test", block: false), +// name: nameNode("Hello") +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleInterfaceWithDescription() throws { +// let source = #""Hello World Interface" interface Hello { world: String } "# +// +// let expected = Document( +// definitions: [ +// InterfaceTypeDefinition( +// description: StringValue(value: "Hello World Interface", block: false), +// name: nameNode("Hello"), +// fields: [ +// fieldNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleUnionWithDescription() throws { +// let source = #""Hello World Union!" union Hello = World "# +// +// let expected = Document( +// definitions: [ +// UnionTypeDefinition( +// description: StringValue(value: "Hello World Union!", block: false), +// name: nameNode("Hello"), +// types: [ +// typeNode("World"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSingleValueEnumDescription() throws { +// let source = #""Hello World Enum..." enum Hello { WORLD } "# +// +// let expected = Document( +// definitions: [ +// EnumTypeDefinition( +// description: StringValue(value: "Hello World Enum...", block: false), +// name: nameNode("Hello"), +// values: [ +// enumValueNode("WORLD"), +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testSimpleInputObjectWithDescription() throws { +// let source = #""Hello Input Object" input Hello { world: String }"# +// +// let expected = Document( +// definitions: [ +// InputObjectTypeDefinition( +// description: StringValue(value: "Hello Input Object", block: false), +// name: nameNode("Hello"), +// fields: [ +// inputValueNode( +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// // Test Fields and values with optional descriptions +// +// func testSingleValueEnumWithDescription() throws { +// let source = """ +// enum Hello { +// "world description" +// WORLD +// "Hello there" +// HELLO +// } +// """ +// +// let expected = Document( +// definitions: [ +// EnumTypeDefinition( +// name: nameNode("Hello"), +// values: [ +// enumValueWithDescriptionNode(StringValue(value: "world description", block: false), "WORLD"), +// enumValueWithDescriptionNode(StringValue(value: "Hello there", block: false), "HELLO") +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testTypeFieldWithDescription() throws { +// let source = #"type Hello { "The world field." world: String } "# +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithDescription( +// StringValue(value: "The world field.", block: false), +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +// func testTypeFieldWithMultilineDescription() throws { +// let source = #""" +// type Hello { +// """ +// The world +// field. +// """ +// world: String +// } +// """# +// +// let expected = Document( +// definitions: [ +// ObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// fieldNodeWithDescription( +// StringValue(value: "The world\nfield.", block: true), +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected, "\(dump(result)) \n\n\(dump(expected))") +// } +// +// +// func testSimpleInputObjectFieldWithDescription() throws { +// let source = #"input Hello { "World field" world: String }"# +// +// let expected = Document( +// definitions: [ +// InputObjectTypeDefinition( +// name: nameNode("Hello"), +// fields: [ +// inputValueWithDescriptionNode( +// StringValue(value: "World field", block: false), +// nameNode("world"), +// typeNode("String") +// ) +// ] +// ) +// ] +// ) +// +// let result = try parse(source: source) +// XCTAssert(result == expected) +// } +// +//} diff --git a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 198d11d9..d078265e 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -2,6 +2,124 @@ import XCTest @testable import GraphQL class VisitorTests : XCTestCase { - func test() throws { + func testVisitsField() throws { + class FieldVisitor: Visitor { + var visited = false + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + visited = true + return .continue + } + } + let document = try! parse(source: """ + query foo { + bar + } + """) + let visitor = FieldVisitor() + visit(root: document, visitor: visitor) + XCTAssert(visitor.visited) + } + + func testVisitorCanEdit() throws { + struct FieldVisitor: Visitor { + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + guard case let .node(queryParent) = parent, queryParent is Selection else { + XCTFail("Unexpected parent") + return .continue + } + var newField = field + newField.name = Name(loc: nil, value: "baz") + return .node(newField) + } + } + let document = try! parse(source: """ + query foo { + bar + } + """) + let visitor = FieldVisitor() + let newDocument = visit(root: document, visitor: visitor) + guard case let .executableDefinition(.operation(opDef)) = newDocument.definitions.first else { + XCTFail("Unexpected definition") + return + } + guard case let .field(field) = opDef.selectionSet.selections.first else { + XCTFail("Unexpected selection") + return + } + XCTAssertEqual("baz", field.name.value) + } + + + func testVisitorCanEditArray() throws { + struct IntIncrementer: Visitor { + func enter(intValue: IntValue, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + let newVal = Int(intValue.value)! + 1 + return .node(IntValue(loc: nil, value: String(newVal))) + } + + func leave(argument: Argument, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if argument.value == .intValue(IntValue(value: "2")) { + return .node(nil) + } + return .continue + } + } + let document = try! parse(source: """ + query foo { + bar(a: 1, b: 2, c: 3) + } + """) + let visitor = IntIncrementer() + let newDocument = visit(root: document, visitor: visitor) + guard case let .executableDefinition(.operation(opDef)) = newDocument.definitions.first else { + XCTFail("Unexpected definition") + return + } + guard case let .field(field) = opDef.selectionSet.selections.first else { + XCTFail("Unexpected selection") + return + } + let expectedInts = [3,4] + for (argument, expected) in zip(field.arguments, expectedInts) { + switch argument.value { + case .intValue(let intVal) where Int(intVal.value) == expected: + break + default: + XCTFail("Unexpected value") + return + } + } + } + + func testVisitorBreaks() { + class FieldVisitor: Visitor { + var visited = false + func enter(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if (visited) { + XCTFail("Visited the nested field and didn't break") + } + visited = true + return .break + } + func leave(field: Field, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + XCTFail("Left the field and didn't break") + return .continue + } + func leave(operationDefinition: OperationDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + XCTFail("Left the operation definition and didn't break") + return .continue + } + } + let document = try! parse(source: """ + { + bar { + baz + } + } + """) + let visitor = FieldVisitor() + visit(root: document, visitor: visitor) + XCTAssert(visitor.visited) } } diff --git a/Tests/GraphQLTests/TypeTests/DecodableTests.swift b/Tests/GraphQLTests/TypeTests/DecodableTests.swift new file mode 100644 index 00000000..20533f72 --- /dev/null +++ b/Tests/GraphQLTests/TypeTests/DecodableTests.swift @@ -0,0 +1,32 @@ +@testable import GraphQL +import XCTest + +class DecodableTests: XCTestCase { + + func testDecodeObjectType() { + let decoder = JSONDecoder() + +// let encoder = JSONEncoder() +// let data = try! encoder.encode(IntrospectionType.scalar(name: "asdf", description: "foo", specifiedByURL: "bar")) +// print(String(data: data, encoding: .utf8)) +// let x = try! decoder.decode(IntrospectionType.self, from: data) +// print(x) + + + let x = try! decoder.decode(AnyIntrospectionType.self, from: """ + { + "kind": "OBJECT", + "name": "Foo" + } + """.data(using: .utf8)!) + print(x) + + + let schemaData = try! Data(contentsOf: URL(fileURLWithPath: "/Users/luke/Desktop/minm-schema.json")) + let z = try! decoder.decode(IntrospectionQuery.self, from: schemaData) + print(z) + let schema = try! buildClientSchema(introspection: z) + print(schema) + } + +} diff --git a/Tests/GraphQLTests/ValidationTests/FieldsOnCorrectTypeTests.swift b/Tests/GraphQLTests/ValidationTests/FieldsOnCorrectTypeTests.swift index 1ff35730..e4e55c78 100644 --- a/Tests/GraphQLTests/ValidationTests/FieldsOnCorrectTypeTests.swift +++ b/Tests/GraphQLTests/ValidationTests/FieldsOnCorrectTypeTests.swift @@ -3,7 +3,7 @@ import XCTest class FieldsOnCorrectTypeTests : ValidationTestCase { override func setUp() { - rule = FieldsOnCorrectTypeRule + rule = FieldsOnCorrectTypeRule.self } func testValidWithObjectFieldSelection() throws { diff --git a/Tests/GraphQLTests/ValidationTests/KnownArgumentNamesTests.swift b/Tests/GraphQLTests/ValidationTests/KnownArgumentNamesTests.swift index 1120c373..f3a5262a 100644 --- a/Tests/GraphQLTests/ValidationTests/KnownArgumentNamesTests.swift +++ b/Tests/GraphQLTests/ValidationTests/KnownArgumentNamesTests.swift @@ -3,7 +3,7 @@ import XCTest class KnownArgumentNamesTests : ValidationTestCase { override func setUp() { - rule = KnownArgumentNamesRule + rule = KnownArgumentNamesRule.self } func testValidWithObjectWithoutArguments() throws { diff --git a/Tests/GraphQLTests/ValidationTests/NoUnusedVariablesRuleTests.swift b/Tests/GraphQLTests/ValidationTests/NoUnusedVariablesRuleTests.swift index 27e62505..e36a644f 100644 --- a/Tests/GraphQLTests/ValidationTests/NoUnusedVariablesRuleTests.swift +++ b/Tests/GraphQLTests/ValidationTests/NoUnusedVariablesRuleTests.swift @@ -3,7 +3,7 @@ import XCTest class NoUnusedVariablesRuleTests : ValidationTestCase { override func setUp() { - rule = NoUnusedVariablesRule + rule = NoUnusedVariablesRule.self } func testUsesAllVariables() throws { diff --git a/Tests/GraphQLTests/ValidationTests/PossibleFragmentSpreadsRuleRuleTests.swift b/Tests/GraphQLTests/ValidationTests/PossibleFragmentSpreadsRuleRuleTests.swift index 8f998865..78a024fd 100644 --- a/Tests/GraphQLTests/ValidationTests/PossibleFragmentSpreadsRuleRuleTests.swift +++ b/Tests/GraphQLTests/ValidationTests/PossibleFragmentSpreadsRuleRuleTests.swift @@ -3,7 +3,7 @@ import XCTest class PossibleFragmentSpreadsRuleRuleTests : ValidationTestCase { override func setUp() { - rule = PossibleFragmentSpreadsRule + rule = PossibleFragmentSpreadsRule.self } func testUsesAllVariables() throws { diff --git a/Tests/GraphQLTests/ValidationTests/ProvidedNonNullArgumentsTests.swift b/Tests/GraphQLTests/ValidationTests/ProvidedNonNullArgumentsTests.swift index 35521a88..115d014c 100644 --- a/Tests/GraphQLTests/ValidationTests/ProvidedNonNullArgumentsTests.swift +++ b/Tests/GraphQLTests/ValidationTests/ProvidedNonNullArgumentsTests.swift @@ -3,7 +3,7 @@ import XCTest class ProvidedNonNullArgumentsTests : ValidationTestCase { override func setUp() { - rule = ProvidedNonNullArgumentsRule + rule = ProvidedNonNullArgumentsRule.self } func testValidWithObjectWithoutArguments() throws { diff --git a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift index c319e17b..574e8c96 100644 --- a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift +++ b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift @@ -2,7 +2,7 @@ import XCTest class ValidationTestCase : XCTestCase { - typealias Rule = (ValidationContext) -> Visitor + typealias Rule = ValidationRule.Type var rule: Rule! @@ -51,5 +51,3 @@ class ValidationTestCase : XCTestCase { } } - - From 75fb18f2b1c60f3a084be97732052414d7a7bd39 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 22 Dec 2021 12:18:22 +0000 Subject: [PATCH 02/19] Remove kinds as they are no longer needed They would be necessary in a duck-typed language such as TypeScript but we can check at runtime with casts in Swift --- Sources/GraphQL/Execution/Execute.swift | 2 +- Sources/GraphQL/Language/AST.swift | 35 ---------------------- Sources/GraphQL/Language/Kinds.swift | 39 ------------------------- 3 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 Sources/GraphQL/Language/Kinds.swift diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 0641bed2..95f34034 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -373,7 +373,7 @@ func buildExecutionContext( fragments[definition.name.value] = definition default: throw GraphQLError( - message: "GraphQL cannot execute a request containing a \(definition.underlyingNode.kind).", + message: "GraphQL cannot execute a request containing a \(type(of: definition)).", nodes: [definition] ) } diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index fcb70388..e472063f 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -201,7 +201,6 @@ extension TypeExtensionDefinition : Node {} extension DirectiveDefinition : Node {} public struct Name { - public let kind: Kind = .name public let loc: Location? public let value: String @@ -227,7 +226,6 @@ extension Name: Hashable { } public struct Document { - public let kind: Kind = .document public let loc: Location? public var definitions: [Definition] @@ -341,7 +339,6 @@ public enum OperationType : String { } public struct OperationDefinition { - public let kind: Kind = .operationDefinition public let loc: Location? public var operation: OperationType public var name: Name? @@ -400,7 +397,6 @@ extension OperationDefinition: Equatable { } public struct VariableDefinition { - public let kind: Kind = .variableDefinition public let loc: Location? public var variable: Variable public var type: Type @@ -453,7 +449,6 @@ extension VariableDefinition : Equatable { } public struct Variable { - public let kind: Kind = .variable public let loc: Location? public var name: Name @@ -479,7 +474,6 @@ extension Variable : Equatable { } public struct SelectionSet { - public let kind: Kind = .selectionSet public let loc: Location? public var selections: [Selection] @@ -550,7 +544,6 @@ public enum Selection: EnumNode, Equatable { } public struct Field { - public let kind: Kind = .field public let loc: Location? public var alias: Name? public var name: Name @@ -608,7 +601,6 @@ extension Field : Equatable { } public struct Argument { - public let kind: Kind = .argument public let loc: Location? public var name: Name public var value: Value @@ -651,7 +643,6 @@ extension Argument : Equatable { } public struct FragmentSpread { - public let kind: Kind = .fragmentSpread public let loc: Location? public var name: Name public var directives: [Directive] @@ -701,7 +692,6 @@ extension FragmentDefinition : HasTypeCondition { } public struct InlineFragment { - public let kind: Kind = .inlineFragment public let loc: Location? public var typeCondition: NamedType? public var directives: [Directive] @@ -744,7 +734,6 @@ extension InlineFragment : Equatable { } public struct FragmentDefinition { - public let kind: Kind = .fragmentDefinition public let loc: Location? public var name: Name public var typeCondition: NamedType @@ -857,7 +846,6 @@ public enum Value: EnumNode, Equatable { } public struct IntValue { - public let kind: Kind = .intValue public let loc: Location? public let value: String @@ -880,7 +868,6 @@ extension IntValue : Equatable { } public struct FloatValue { - public let kind: Kind = .floatValue public let loc: Location? public let value: String @@ -903,7 +890,6 @@ extension FloatValue : Equatable { } public struct StringValue { - public let kind: Kind = .stringValue public let loc: Location? public let value: String public let block: Bool? @@ -935,7 +921,6 @@ extension StringValue : Equatable { } public struct BooleanValue { - public let kind: Kind = .booleanValue public let loc: Location? public let value: Bool @@ -958,7 +943,6 @@ extension BooleanValue : Equatable { } public struct NullValue { - public let kind: Kind = .nullValue public let loc: Location? init(loc: Location? = nil) { @@ -979,7 +963,6 @@ extension NullValue : Equatable { } public struct EnumValue { - public let kind: Kind = .enumValue public let loc: Location? public let value: String @@ -1002,7 +985,6 @@ extension EnumValue : Equatable { } public struct ListValue { - public let kind: Kind = .listValue public let loc: Location? public var values: [Value] @@ -1044,7 +1026,6 @@ extension ListValue : Equatable { } public struct ObjectValue { - public let kind: Kind = .objectValue public let loc: Location? public var fields: [ObjectField] @@ -1077,7 +1058,6 @@ extension ObjectValue : Equatable { } public struct ObjectField { - public let kind: Kind = .objectField public let loc: Location? public var name: Name public var value: Value @@ -1108,7 +1088,6 @@ extension ObjectField : Equatable { } public struct Directive { - public let kind: Kind = .directive public let loc: Location? public var name: Name public var arguments: [Argument] @@ -1186,7 +1165,6 @@ public indirect enum Type: EnumNode, Equatable { } public struct NamedType { - public let kind: Kind = .namedType public let loc: Location? public var name: Name @@ -1211,7 +1189,6 @@ extension NamedType : Equatable { } public struct ListType { - public let kind: Kind = .listType public let loc: Location? public var type: Type @@ -1340,7 +1317,6 @@ extension SchemaDefinition : Equatable { } public struct OperationTypeDefinition { - public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType public var type: NamedType @@ -1417,7 +1393,6 @@ public enum TypeDefinition: EnumNode, Equatable { } public struct ScalarTypeDefinition { - public let kind: Kind = .scalarTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1450,7 +1425,6 @@ extension ScalarTypeDefinition : Equatable { } public struct ObjectTypeDefinition { - public let kind: Kind = .objectTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1491,7 +1465,6 @@ extension ObjectTypeDefinition : Equatable { } public struct FieldDefinition { - public let kind: Kind = .fieldDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1532,7 +1505,6 @@ extension FieldDefinition : Equatable { } public struct InputValueDefinition { - public let kind: Kind = .inputValueDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1589,7 +1561,6 @@ extension InputValueDefinition : Equatable { } public struct InterfaceTypeDefinition { - public let kind: Kind = .interfaceTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1636,7 +1607,6 @@ extension InterfaceTypeDefinition : Equatable { } public struct UnionTypeDefinition { - public let kind: Kind = .unionTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1673,7 +1643,6 @@ extension UnionTypeDefinition : Equatable { } public struct EnumTypeDefinition { - public let kind: Kind = .enumTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1710,7 +1679,6 @@ extension EnumTypeDefinition : Equatable { } public struct EnumValueDefinition { - public let kind: Kind = .enumValueDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1743,7 +1711,6 @@ extension EnumValueDefinition : Equatable { } public struct InputObjectTypeDefinition { - public let kind: Kind = .inputObjectTypeDefinition public let loc: Location? public var description: StringValue? public var name: Name @@ -1780,7 +1747,6 @@ extension InputObjectTypeDefinition : Equatable { } public struct TypeExtensionDefinition { - public let kind: Kind = .typeExtensionDefinition public let loc: Location? public var definition: ObjectTypeDefinition @@ -1805,7 +1771,6 @@ extension TypeExtensionDefinition : Equatable { } public struct DirectiveDefinition { - public let kind: Kind = .directiveDefinition public let loc: Location? public var description: StringValue? public var name: Name diff --git a/Sources/GraphQL/Language/Kinds.swift b/Sources/GraphQL/Language/Kinds.swift deleted file mode 100644 index a1c3e1c5..00000000 --- a/Sources/GraphQL/Language/Kinds.swift +++ /dev/null @@ -1,39 +0,0 @@ -public enum Kind { - case name - case document - case operationDefinition - case variableDefinition - case variable - case selectionSet - case field - case argument - case fragmentSpread - case inlineFragment - case fragmentDefinition - case intValue - case floatValue - case stringValue - case booleanValue - case nullValue - case enumValue - case listValue - case objectValue - case objectField - case directive - case namedType - case listType - case nonNullType - case schemaDefinition - case operationTypeDefinition - case scalarTypeDefinition - case objectTypeDefinition - case fieldDefinition - case inputValueDefinition - case interfaceTypeDefinition - case unionTypeDefinition - case enumTypeDefinition - case enumValueDefinition - case inputObjectTypeDefinition - case typeExtensionDefinition - case directiveDefinition -} From f12c6ba18e137934c348d0bfaf0ad27d0e54ebc2 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 26 Jan 2022 19:39:35 +0000 Subject: [PATCH 03/19] WIP --- Sources/GraphQL/Language/AST.swift | 35 ++++++--- Sources/GraphQL/Language/Visitor.swift | 73 +++++++------------ Sources/GraphQL/Type/Definition.swift | 11 +-- .../GraphQL/Utilities/BuildClientSchema.swift | 9 ++- .../Utilities/GetIntrospectionQuery.swift | 2 +- .../GraphQL/Utilities/TypeComparators.swift | 4 +- Sources/GraphQL/Utilities/TypeFromAST.swift | 2 +- 7 files changed, 71 insertions(+), 65 deletions(-) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index e472063f..f4eb820a 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -142,7 +142,7 @@ public protocol Node: TextOutputStreamable { } extension Node { - var printed: String { + public var printed: String { var s = "" self.write(to: &s) return s @@ -248,17 +248,34 @@ public struct Document { extension Document : Equatable { public static func == (lhs: Document, rhs: Document) -> Bool { - guard lhs.definitions.count == rhs.definitions.count else { - return false - } + return lhs.definitions == rhs.definitions + } +} - for (l, r) in zip(lhs.definitions, rhs.definitions) { - guard l == r else { - return false - } +public struct ExecutableDocument { + public let loc: Location? + public var definitions: [ExecutableDefinition] + + init(loc: Location? = nil, definitions: [ExecutableDefinition]) { + self.loc = loc + self.definitions = definitions + } + + public mutating func descend(descender: inout Descender) { + descender.descend(&self, \.definitions) + } + + public func write(to target: inout Target) where Target : TextOutputStream { + definitions.forEach { + $0.write(to: &target) + target.write("\n\n") } + } +} - return true +extension ExecutableDocument: Equatable { + public static func == (lhs: ExecutableDocument, rhs: ExecutableDocument) -> Bool { + return lhs.definitions == rhs.definitions } } diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 95412365..5df2015f 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -1,17 +1,19 @@ public struct Descender { - enum Mutation { - case replace(T) - case remove - } private let visitor: Visitor fileprivate var parentStack: [VisitorParent] = [] private var path: [AnyKeyPath] = [] private var isBreaking = false - fileprivate mutating func go(node: inout H, key: AnyKeyPath?) -> Mutation? { - if isBreaking { return nil } + /** + The meat of the traversal + + - Returns + `true` if node should be removed + */ + fileprivate mutating func go(node: inout H, key: AnyKeyPath?) -> Bool { + if isBreaking { return false } let parent = parentStack.last let newPath: [AnyKeyPath] if let key = key { @@ -20,23 +22,23 @@ public struct Descender { newPath = path } - var mutation: Mutation? = nil + var shouldRemove = false switch visitor.enter(node: node, key: key, parent: parent, path: newPath, ancestors: parentStack) { case .skip: - return nil// .node(Optional(result)) + return false case .continue: break case let .node(newNode): if let newNode = newNode { - mutation = .replace(newNode) + node = newNode } else { // TODO: Should we still be traversing the children here? - mutation = .remove + shouldRemove = true } case .break: isBreaking = true - return nil + return false } parentStack.append(.node(node)) if let key = key { @@ -48,32 +50,28 @@ public struct Descender { } parentStack.removeLast() - if isBreaking { return mutation } + if isBreaking { return shouldRemove } switch visitor.leave(node: node, key: key, parent: parent, path: newPath, ancestors: parentStack) { case .skip, .continue: - return mutation + return shouldRemove case let .node(newNode): if let newNode = newNode { - return .replace(newNode) + node = newNode + return false } else { // TODO: Should we still be traversing the children here? - return .remove + return true } case .break: isBreaking = true - return mutation + return shouldRemove } } mutating func descend(_ node: inout T, _ kp: WritableKeyPath) { - switch go(node: &node[keyPath: kp], key: kp) { - case nil: - break - case .replace(let child): - node[keyPath: kp] = child - case .remove: + if go(node: &node[keyPath: kp], key: kp) { fatalError("Can't remove this node") } } @@ -81,13 +79,10 @@ public struct Descender { guard var oldVal = node[keyPath: kp] else { return } - switch go(node: &oldVal, key: kp) { - case nil: - node[keyPath: kp] = oldVal - case .replace(let child): - node[keyPath: kp] = child - case .remove: + if go(node: &oldVal, key: kp) { node[keyPath: kp] = nil + } else { + node[keyPath: kp] = oldVal } } mutating func descend(_ node: inout T, _ kp: WritableKeyPath) { @@ -97,12 +92,7 @@ public struct Descender { var i = node[keyPath: kp].startIndex while i != node[keyPath: kp].endIndex { - switch go(node: &node[keyPath: kp][i], key: \[U].[i]) { - case nil: - break - case .replace(let child): - node[keyPath: kp][i] = child - case .remove: + if go(node: &node[keyPath: kp][i], key: \[U].[i]) { toRemove.append(i) } i = node[keyPath: kp].index(after: i) @@ -112,12 +102,7 @@ public struct Descender { } mutating func descend(enumCase: inout T) { - switch go(node: &enumCase, key: nil) { - case nil: - break - case .replace(let node): - enumCase = node - case .remove: + if go(node: &enumCase, key: nil) { //TODO: figure this out fatalError("What happens here?") } @@ -169,14 +154,10 @@ public func visit(root: T, visitor: V) -> T { var descender = Descender(visitor: visitor) var result = root - switch descender.go(node: &result, key: nil) { - case .remove: + if descender.go(node: &result, key: nil) { fatalError("Root node in the AST was removed") - case .replace(let node): - return node - case nil: - return result } + return result } public enum VisitorParent { diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index f7d15fc9..f8e82132 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -915,11 +915,12 @@ public final class GraphQLUnionType { public let resolveType: GraphQLTypeResolve? private let typesThunk: () -> [GraphQLObjectType] public lazy var types = { - try! defineTypes( - name: name, - hasResolve: resolveType != nil, - types: typesThunk() - ) + typesThunk() +// try! defineTypes( +// name: name, +// hasResolve: resolveType != nil, +// types: typesThunk() +// ) }() public let possibleTypeNames: [String: Bool] public let kind: TypeKind = .union diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index c53e9cc7..2cdb52ec 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -152,9 +152,16 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph // let directives = [] + let mutationType: GraphQLObjectType? + if let mutationTypeRef = schemaIntrospection.mutationType { + mutationType = try getObjectType(name: mutationTypeRef.name) + } else { + mutationType = nil + } + return try GraphQLSchema( query: try getObjectType(name: schemaIntrospection.queryType.name), - mutation: nil, + mutation: mutationType, subscription: nil, types: Array(typeMap.values), directives: [] diff --git a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift index d5ef6790..720281c0 100644 --- a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift +++ b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift @@ -1,4 +1,4 @@ -func getIntrospectionQuery( +public func getIntrospectionQuery( descriptions: Bool = true, specifiedByUrl: Bool = false, directiveIsRepeatable: Bool = false, diff --git a/Sources/GraphQL/Utilities/TypeComparators.swift b/Sources/GraphQL/Utilities/TypeComparators.swift index 2c686b1a..ee754f8c 100644 --- a/Sources/GraphQL/Utilities/TypeComparators.swift +++ b/Sources/GraphQL/Utilities/TypeComparators.swift @@ -1,7 +1,7 @@ /** * Provided two types, return true if the types are equal (invariant). */ -func isEqualType(_ typeA: GraphQLType, _ typeB: GraphQLType) -> Bool { +public func isEqualType(_ typeA: GraphQLType, _ typeB: GraphQLType) -> Bool { // Equivalent types are equal. if typeA == typeB { return true @@ -70,7 +70,7 @@ func == (lhs: GraphQLType, rhs: GraphQLType) -> Bool { * Provided a type and a super type, return true if the first type is either * equal or a subset of the second super type (covariant). */ -func isTypeSubTypeOf( +public func isTypeSubTypeOf( _ schema: GraphQLSchema, _ maybeSubType: GraphQLType, _ superType: GraphQLType diff --git a/Sources/GraphQL/Utilities/TypeFromAST.swift b/Sources/GraphQL/Utilities/TypeFromAST.swift index e1626c68..a187f207 100644 --- a/Sources/GraphQL/Utilities/TypeFromAST.swift +++ b/Sources/GraphQL/Utilities/TypeFromAST.swift @@ -1,4 +1,4 @@ -func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> GraphQLType? { +public func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> GraphQLType? { switch inputTypeAST { case let .listType(listType): if let innerType = typeFromAST(schema: schema, inputTypeAST: listType.type) { From 13a95b0e38d02ba6f496a5efb20239b1e38a5eb4 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Tue, 22 Feb 2022 19:23:14 +0000 Subject: [PATCH 04/19] Lazily reoslve interface's interfaces in BuildClientSchema --- Sources/GraphQL/Type/Definition.swift | 11 +++++++---- Sources/GraphQL/Utilities/BuildClientSchema.swift | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index f8e82132..8605616f 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -791,7 +791,10 @@ public final class GraphQLInterfaceType { fields: fieldsThunk() ) }() - public let interfaces: [GraphQLInterfaceType] + public lazy var interfaces: [GraphQLInterfaceType] = { + try! interfacesThunk() + }() + private let interfacesThunk: () throws -> [GraphQLInterfaceType] public let kind: TypeKind = .interface public convenience init( @@ -804,7 +807,7 @@ public final class GraphQLInterfaceType { try self.init( name: name, description: description, - interfaces: interfaces, + interfaces: { interfaces }, fields: { fields }, resolveType: resolveType ) @@ -813,7 +816,7 @@ public final class GraphQLInterfaceType { public init( name: String, description: String? = nil, - interfaces: [GraphQLInterfaceType] = [], + interfaces: @escaping () throws -> [GraphQLInterfaceType], fields: @escaping () -> GraphQLFieldMap, resolveType: GraphQLTypeResolve? = nil ) throws { @@ -823,7 +826,7 @@ public final class GraphQLInterfaceType { self.fieldsThunk = fields - self.interfaces = interfaces + self.interfacesThunk = interfaces self.resolveType = resolveType } diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index 2cdb52ec..28f4bda3 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -105,7 +105,7 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph return try GraphQLInterfaceType( name: type.name, description: type.description, - interfaces: buildImplementationsList(interfaces: type.interfaces ?? []), + interfaces: { try buildImplementationsList(interfaces: type.interfaces ?? []) }, fields: { try! buildFieldDefMap(fields: type.fields ?? []) }, resolveType: nil ) From 64952189b9d45121bb88693cebf9795adcc77d20 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 23 Feb 2022 14:15:39 +0000 Subject: [PATCH 05/19] Fix doTypesOverlap when seeing if object overlaps with interface It's possible here that typeA is also an interface --- Sources/GraphQL/Utilities/TypeComparators.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/GraphQL/Utilities/TypeComparators.swift b/Sources/GraphQL/Utilities/TypeComparators.swift index ee754f8c..dd05be3b 100644 --- a/Sources/GraphQL/Utilities/TypeComparators.swift +++ b/Sources/GraphQL/Utilities/TypeComparators.swift @@ -171,10 +171,7 @@ func doTypesOverlap( } } - if - let typeA = typeA as? GraphQLObjectType, - let typeB = typeB as? GraphQLAbstractType - { + if let typeB = typeB as? GraphQLAbstractType { // Determine if the former type is a possible concrete type of the latter. return schema.isSubType( abstractType: typeB, From ac2a4edfa2b391bed67788be1a03de17d22698a0 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 23 Feb 2022 14:39:49 +0000 Subject: [PATCH 06/19] Handle interface implementations of other interfaces --- Sources/GraphQL/Type/Schema.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index 0fff7452..fa5f3ac6 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -191,18 +191,15 @@ func collectImplementations( ) -> [String : InterfaceImplementations] { var implementations: [String: InterfaceImplementations] = [:] - for type in types { + var typesToCheck = types + while let type = typesToCheck.popLast() { if let type = type as? GraphQLInterfaceType { if implementations[type.name] == nil { implementations[type.name] = InterfaceImplementations() } - // Store implementations by interface. - for iface in type.interfaces { - implementations[iface.name] = InterfaceImplementations( - interfaces: (implementations[iface.name]?.interfaces ?? []) + [type] - ) - } + // Depth first search of other interface implementations + typesToCheck += type.interfaces } if let type = type as? GraphQLObjectType { From ac9fbbce59ef0a35c1fad44dc1771225588b9d07 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Thu, 24 Feb 2022 20:18:45 +0000 Subject: [PATCH 07/19] Add variables are input types validation rule --- .../Rules/VariablesAreInputTypesRule.swift | 21 +++++++++++++++++++ .../GraphQL/Validation/SpecifiedRules.swift | 1 + 2 files changed, 22 insertions(+) create mode 100644 Sources/GraphQL/Validation/Rules/VariablesAreInputTypesRule.swift diff --git a/Sources/GraphQL/Validation/Rules/VariablesAreInputTypesRule.swift b/Sources/GraphQL/Validation/Rules/VariablesAreInputTypesRule.swift new file mode 100644 index 00000000..9ffcbd5b --- /dev/null +++ b/Sources/GraphQL/Validation/Rules/VariablesAreInputTypesRule.swift @@ -0,0 +1,21 @@ +/** + * Variables are input types + * + * A GraphQL operation is only valid if all the variables it defines are of + * input types (scalar, enum, or input object). + * + * See https://spec.graphql.org/draft/#sec-Variables-Are-Input-Types + */ +struct VariablesAreInputTypesRule: ValidationRule { + let context: ValidationContext + func enter(variableDefinition: VariableDefinition, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + if context.inputType == nil { + let error = GraphQLError( + message: "Variable \"$\(variableDefinition.variable.name.value)\" cannot be non-input type \"\(variableDefinition.type.printed)\".", + nodes: [variableDefinition.type] + ) + context.report(error: error) + } + return .continue + } +} diff --git a/Sources/GraphQL/Validation/SpecifiedRules.swift b/Sources/GraphQL/Validation/SpecifiedRules.swift index 6d28c360..aa9a2d8f 100644 --- a/Sources/GraphQL/Validation/SpecifiedRules.swift +++ b/Sources/GraphQL/Validation/SpecifiedRules.swift @@ -26,4 +26,5 @@ let specifiedRules: [ValidationRule.Type] = [ // VariablesInAllowedPosition, // OverlappingFieldsCanBeMerged, // UniqueInputFieldNames, + VariablesAreInputTypesRule.self ] From f1d3e38792f2072752c2f6aba757c1531b628690 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 23 Mar 2022 10:06:40 +0000 Subject: [PATCH 08/19] Fix introspection query encoding --- Package.resolved | 4 ++-- Package.swift | 2 +- .../Utilities/GetIntrospectionQuery.swift | 20 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Package.resolved b/Package.resolved index 4b4f5302..2dfbef0e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "120acb15c39aa3217e9888e515de160378fbcc1e", - "version": "2.18.0" + "revision": "154f1d32366449dcccf6375a173adf4ed2a74429", + "version": "2.38.0" } } ] diff --git a/Package.swift b/Package.swift index c05a35e9..a4c4e9bd 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( .library(name: "GraphQL", targets: ["GraphQL"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")), + .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.38.0")), .package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.0.0")), ], targets: [ diff --git a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift index 720281c0..4c996ac1 100644 --- a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift +++ b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift @@ -124,13 +124,13 @@ struct IntrospectionSchema: Codable { let types: [AnyIntrospectionType] // let directives: [IntrospectionDirective] - func encode(to encoder: Encoder) throws { - try types.encode(to: encoder) - } +// func encode(to encoder: Encoder) throws { +// try types.encode(to: encoder) +// } } protocol IntrospectionType: Codable { - static var kind: TypeKind2 { get } + var kind: TypeKind2 { get } var name: String { get } } @@ -212,14 +212,14 @@ struct AnyIntrospectionType: Codable { //extension IntrospectionInputObjectType: IntrospectionInputType {} // struct IntrospectionScalarType: IntrospectionType { - static let kind = TypeKind2.scalar + var kind = TypeKind2.scalar let name: String let description: String? let specifiedByURL: String? } struct IntrospectionObjectType: IntrospectionType { - static let kind = TypeKind2.object + var kind = TypeKind2.object let name: String let description: String? let fields: [IntrospectionField]? @@ -227,7 +227,7 @@ struct IntrospectionObjectType: IntrospectionType { } struct IntrospectionInterfaceType: IntrospectionType { - static let kind = TypeKind2.interface + var kind = TypeKind2.interface let name: String let description: String? let fields: [IntrospectionField]? @@ -236,21 +236,21 @@ struct IntrospectionInterfaceType: IntrospectionType { } struct IntrospectionUnionType: IntrospectionType { - static let kind = TypeKind2.union + var kind = TypeKind2.union let name: String let description: String? let possibleTypes: [IntrospectionTypeRef] } struct IntrospectionEnumType: IntrospectionType { - static let kind = TypeKind2.enum + var kind = TypeKind2.enum let name: String let description: String? let enumValues: [IntrospectionEnumValue] } struct IntrospectionInputObjectType: IntrospectionType { - static let kind = TypeKind2.inputObject + var kind = TypeKind2.inputObject let name: String let description: String? let inputFields: [IntrospectionInputValue] From 8a71abf4b61a387327de3daadba88dfcda6d3170 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Wed, 20 Apr 2022 11:06:23 +0100 Subject: [PATCH 09/19] Add specifiedByURL to scalar types --- Sources/GraphQL/Type/Definition.swift | 5 +++++ Sources/GraphQL/Type/Introspection.swift | 15 ++++++--------- Sources/GraphQL/Utilities/BuildClientSchema.swift | 1 + .../GraphQL/Utilities/GetIntrospectionQuery.swift | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 8605616f..e8e87f44 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -165,6 +165,7 @@ extension GraphQLNonNull : GraphQLWrapperType {} public final class GraphQLScalarType { public let name: String public let description: String? + public let specifiedByURL: String? public let kind: TypeKind = .scalar let serialize: (Any) throws -> Map @@ -174,11 +175,13 @@ public final class GraphQLScalarType { public init( name: String, description: String? = nil, + specifiedByURL: String? = nil, serialize: @escaping (Any) throws -> Map ) throws { try assertValid(name: name) self.name = name self.description = description + self.specifiedByURL = specifiedByURL self.serialize = serialize self.parseValue = nil self.parseLiteral = nil @@ -187,6 +190,7 @@ public final class GraphQLScalarType { public init( name: String, description: String? = nil, + specifiedByURL: String? = nil, serialize: @escaping (Any) throws -> Map, parseValue: @escaping (Map) throws -> Map, parseLiteral: @escaping (Value) throws -> Map @@ -194,6 +198,7 @@ public final class GraphQLScalarType { try assertValid(name: name) self.name = name self.description = description + self.specifiedByURL = specifiedByURL self.serialize = serialize self.parseValue = parseValue self.parseLiteral = parseLiteral diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index cf14c4e7..d748ac9a 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -179,15 +179,11 @@ let __DirectiveLocation = try! GraphQLEnumType( let __Type: GraphQLObjectType = try! GraphQLObjectType( name: "__Type", - description: - "The fundamental unit of any GraphQL Schema is the type. There are " + - "many kinds of types in GraphQL as represented by the `__TypeKind` enum." + - "\n\nDepending on the kind of a type, certain fields describe " + - "information about that type. Scalar types provide no information " + - "beyond a name and description, while Enum types provide their values. " + - "Object and Interface types provide the fields they describe. Abstract " + - "types, Union and Interface, provide the Object types possible " + - "at runtime. List and NonNull types compose other types.", + description: """ + The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + + Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. + """, fields: [ "kind": GraphQLField( type: GraphQLNonNull(__TypeKind), @@ -216,6 +212,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType( ), "name": GraphQLField(type: GraphQLString), "description": GraphQLField(type: GraphQLString), + "specifiedByURL": GraphQLField(type: GraphQLString), "fields": GraphQLField( type: GraphQLList(GraphQLNonNull(__Field)), args: [ diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index 28f4bda3..73c0b5c1 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -92,6 +92,7 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph return try GraphQLScalarType( name: type.name, description: type.description, + specifiedByURL: type.specifiedByURL, serialize: { try map(from: $0) } ) case let type as IntrospectionObjectType: diff --git a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift index 4c996ac1..2da0356d 100644 --- a/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift +++ b/Sources/GraphQL/Utilities/GetIntrospectionQuery.swift @@ -1,13 +1,13 @@ public func getIntrospectionQuery( descriptions: Bool = true, - specifiedByUrl: Bool = false, + specifiedByURL: Bool = false, directiveIsRepeatable: Bool = false, schemaDescription: Bool = false, inputValueDeprecation: Bool = false ) -> String { let descriptions = descriptions ? "description" : "" - let specifiedByUrl = specifiedByUrl ? "specifiedByURL" : "" + let specifiedByURL = specifiedByURL ? "specifiedByURL" : "" let directiveIsRepeatable = directiveIsRepeatable ? "isRepeatable" : "" let schemaDescription = schemaDescription ? descriptions : "" @@ -40,7 +40,7 @@ public func getIntrospectionQuery( kind name \(descriptions) - \(specifiedByUrl) + \(specifiedByURL) fields(includeDeprecated: true) { name \(descriptions) From 0765b9ff515ce74f76f85f3dd1b21f8304447019 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Thu, 9 Jun 2022 13:07:56 +0100 Subject: [PATCH 10/19] Make subtyping reflexive --- Sources/GraphQL/Type/Definition.swift | 2 +- Sources/GraphQL/Type/Schema.swift | 6 +++++- Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index e8e87f44..43d9d6b1 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -84,7 +84,7 @@ extension GraphQLInputObjectType : GraphQLTypeReferenceContainer {} /** * These types may describe the parent context of a selection set. */ -public protocol GraphQLAbstractType : GraphQLNamedType { +public protocol GraphQLAbstractType : GraphQLCompositeType { var resolveType: GraphQLTypeResolve? { get } } diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index fa5f3ac6..1434671e 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -120,13 +120,17 @@ public final class GraphQLSchema { } public func isSubType( - abstractType: GraphQLAbstractType, + abstractType: GraphQLCompositeType, maybeSubType: GraphQLNamedType ) -> Bool { var map = subTypeMap[abstractType.name] if map == nil { map = [:] + + if let objectType = abstractType as? GraphQLObjectType { + map?[objectType.name] = true + } if let unionType = abstractType as? GraphQLUnionType { for type in unionType.types { diff --git a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift index 7a77dcf2..ec075a4b 100644 --- a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift +++ b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift @@ -151,4 +151,14 @@ class GraphQLSchemaTests: XCTestCase { XCTAssertEqual(graphQLError.message, "Object.fieldWithoutArg includes required argument (addedRequiredArg:) that is missing from the Interface field Interface.fieldWithoutArg.") } } + + func testSubtypingIsReflexive() throws { + let object = try GraphQLObjectType( + name: "Object", + fields: ["foo": GraphQLField(type: GraphQLInt)], + interfaces: [] + ) + let schema = try GraphQLSchema(query: object, types: [object]) + XCTAssert(schema.isSubType(abstractType: object, maybeSubType: object)) + } } From 39338509fca759caa6abea1dad3eb5977dc40aae Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Thu, 9 Jun 2022 15:03:55 +0100 Subject: [PATCH 11/19] Make GraphQLType Equatable To do so, need to make it conform to AnyObject, which in turn forces us to use Swift 5.6 any syntax for existential types throughout --- Sources/GraphQL/Execution/Execute.swift | 16 ++-- Sources/GraphQL/Execution/Values.swift | 10 +-- Sources/GraphQL/Type/Definition.swift | 76 ++++++++++--------- Sources/GraphQL/Type/Introspection.swift | 4 +- Sources/GraphQL/Type/Schema.swift | 32 ++++---- Sources/GraphQL/Utilities/ASTFromValue.swift | 8 +- .../GraphQL/Utilities/BuildClientSchema.swift | 14 ++-- Sources/GraphQL/Utilities/IsValidValue.swift | 8 +- .../GraphQL/Utilities/TypeComparators.swift | 22 +++--- Sources/GraphQL/Utilities/TypeFromAST.swift | 4 +- Sources/GraphQL/Utilities/TypeInfo.swift | 32 ++++---- Sources/GraphQL/Utilities/ValueFromAST.swift | 8 +- .../Rules/FieldsOnCorrectTypeRule.swift | 6 +- .../Rules/PossibleFragmentSpreadsRule.swift | 6 +- .../Validation/Rules/ScalarLeafsRule.swift | 4 +- Sources/GraphQL/Validation/Validate.swift | 8 +- 16 files changed, 132 insertions(+), 126 deletions(-) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 95f34034..97e90e1c 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -633,7 +633,7 @@ func doesFragmentConditionMatch( return true } - if let abstractType = conditionalType as? GraphQLAbstractType { + if let abstractType = conditionalType as? (any GraphQLAbstractType) { return exeContext.schema.isSubType( abstractType: abstractType, maybeSubType: type @@ -761,7 +761,7 @@ func resolveOrError( // in the execution context. func completeValueCatchingError( exeContext: ExecutionContext, - returnType: GraphQLType, + returnType: any GraphQLType, fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, @@ -813,7 +813,7 @@ func completeValueCatchingError( // location information. func completeValueWithLocatedError( exeContext: ExecutionContext, - returnType: GraphQLType, + returnType: any GraphQLType, fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, @@ -863,7 +863,7 @@ func completeValueWithLocatedError( */ func completeValue( exeContext: ExecutionContext, - returnType: GraphQLType, + returnType: any GraphQLType, fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, @@ -995,7 +995,7 @@ func completeListValue( * Complete a Scalar or Enum by serializing to a valid value, returning * .null if serialization is not possible. */ -func completeLeafValue(returnType: GraphQLLeafType, result: Any?) throws -> Map { +func completeLeafValue(returnType: any GraphQLLeafType, result: Any?) throws -> Map { guard let result = result else { return .null } @@ -1019,7 +1019,7 @@ func completeLeafValue(returnType: GraphQLLeafType, result: Any?) throws -> Map */ func completeAbstractValue( exeContext: ExecutionContext, - returnType: GraphQLAbstractType, + returnType: any GraphQLAbstractType, fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, @@ -1042,7 +1042,7 @@ func completeAbstractValue( } // If resolveType returns a string, we assume it's a GraphQLObjectType name. - var runtimeType: GraphQLType? + var runtimeType: (any GraphQLType)? switch resolveResult { case .name(let name): @@ -1139,7 +1139,7 @@ func defaultResolveType( value: Any, eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, - abstractType: GraphQLAbstractType + abstractType: any GraphQLAbstractType ) throws -> TypeResolveResult? { let possibleTypes = info.schema.getPossibleTypes(abstractType: abstractType) diff --git a/Sources/GraphQL/Execution/Values.swift b/Sources/GraphQL/Execution/Values.swift index 0336f4e4..d314a913 100644 --- a/Sources/GraphQL/Execution/Values.swift +++ b/Sources/GraphQL/Execution/Values.swift @@ -82,7 +82,7 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition, let type = typeFromAST(schema: schema, inputTypeAST: definitionAST.type) let variable = definitionAST.variable - guard let inputType = type as? GraphQLInputType else { + guard let inputType = type as? (any GraphQLInputType) else { throw GraphQLError( message: "Variable \"$\(variable.name.value)\" expected value of type " + @@ -112,11 +112,11 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition, /** * Given a type and any value, return a runtime value coerced to match the type. */ -func coerceValue(value: Map, type: GraphQLInputType) throws -> Map { +func coerceValue(value: Map, type: any GraphQLInputType) throws -> Map { if let nonNull = type as? GraphQLNonNull { // Note: we're not checking that the result of coerceValue is non-null. // We only call this function after calling validate. - guard let nonNullType = nonNull.ofType as? GraphQLInputType else { + guard let nonNullType = nonNull.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "NonNull must wrap an input type") } return try coerceValue(value: value, type: nonNullType) @@ -127,7 +127,7 @@ func coerceValue(value: Map, type: GraphQLInputType) throws -> Map { } if let list = type as? GraphQLList { - guard let itemType = list.ofType as? GraphQLInputType else { + guard let itemType = list.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "Input list must wrap an input type") } @@ -168,7 +168,7 @@ func coerceValue(value: Map, type: GraphQLInputType) throws -> Map { return .dictionary(object) } - if let leafType = type as? GraphQLLeafType { + if let leafType = type as? (any GraphQLLeafType) { return try leafType.parseValue(value: value) } diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 43d9d6b1..f58244a0 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -4,7 +4,7 @@ import NIO /** * These are all of the possible kinds of types. */ -public protocol GraphQLType : CustomDebugStringConvertible, Encodable, KeySubscriptable {} +public protocol GraphQLType : CustomDebugStringConvertible, Encodable, KeySubscriptable, AnyObject, Equatable {} extension GraphQLScalarType : GraphQLType {} extension GraphQLObjectType : GraphQLType {} extension GraphQLInterfaceType : GraphQLType {} @@ -14,6 +14,12 @@ extension GraphQLInputObjectType : GraphQLType {} extension GraphQLList : GraphQLType {} extension GraphQLNonNull : GraphQLType {} +extension GraphQLType { + public static func == (lhs: Self, rhs: Self) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + /** * These types may be used as input types for arguments and directives. */ @@ -27,9 +33,9 @@ extension GraphQLNonNull : GraphQLInputType {} //extension GraphQLList : GraphQLInputType where Element : GraphQLInputType {} //extension GraphQLNonNull : GraphQLInputType where Element : (GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList) {} -func isInputType(type: GraphQLType?) -> Bool { +func isInputType(type: (any GraphQLType)?) -> Bool { let namedType = getNamedType(type: type) - return namedType is GraphQLInputType + return namedType is any GraphQLInputType } /** @@ -59,7 +65,7 @@ public protocol GraphQLLeafType : GraphQLNamedType { extension GraphQLScalarType : GraphQLLeafType {} extension GraphQLEnumType : GraphQLLeafType {} -func isLeafType(type: GraphQLType?) -> Bool { +func isLeafType(type: (any GraphQLType)?) -> Bool { let namedType = getNamedType(type: type) return namedType is GraphQLScalarType || namedType is GraphQLEnumType @@ -103,12 +109,12 @@ extension GraphQLEnumType : GraphQLNullableType {} extension GraphQLInputObjectType : GraphQLNullableType {} extension GraphQLList : GraphQLNullableType {} -func getNullableType(type: GraphQLType?) -> GraphQLNullableType? { +func getNullableType(type: (any GraphQLType)?) -> (any GraphQLNullableType)? { if let type = type as? GraphQLNonNull { return type.ofType } - return type as? GraphQLNullableType + return type as? any GraphQLNullableType } /** @@ -125,21 +131,21 @@ extension GraphQLUnionType : GraphQLNamedType {} extension GraphQLEnumType : GraphQLNamedType {} extension GraphQLInputObjectType : GraphQLNamedType {} -public func getNamedType(type: GraphQLType?) -> GraphQLNamedType? { +public func getNamedType(type: (any GraphQLType)?) -> (any GraphQLNamedType)? { var unmodifiedType = type - while let type = unmodifiedType as? GraphQLWrapperType { + while let type = unmodifiedType as? (any GraphQLWrapperType) { unmodifiedType = type.wrappedType } - return unmodifiedType as? GraphQLNamedType + return unmodifiedType as? (any GraphQLNamedType) } /** * These types wrap other types. */ protocol GraphQLWrapperType : GraphQLType { - var wrappedType: GraphQLType { get } + var wrappedType: any GraphQLType { get } } extension GraphQLList : GraphQLWrapperType {} @@ -536,8 +542,8 @@ public typealias GraphQLFieldResolveInput = ( public struct GraphQLResolveInfo { public let fieldName: String public let fieldASTs: [Field] - public let returnType: GraphQLOutputType - public let parentType: GraphQLCompositeType + public let returnType: any GraphQLOutputType + public let parentType: any GraphQLCompositeType public let path: IndexPath public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] @@ -549,7 +555,7 @@ public struct GraphQLResolveInfo { public typealias GraphQLFieldMap = [String: GraphQLField] public struct GraphQLField { - public let type: GraphQLOutputType + public let type: any GraphQLOutputType public let args: GraphQLArgumentConfigMap public let deprecationReason: String? public let description: String? @@ -557,7 +563,7 @@ public struct GraphQLField { public let subscribe: GraphQLFieldResolve? public init( - type: GraphQLOutputType, + type: any GraphQLOutputType, description: String? = nil, deprecationReason: String? = nil, args: GraphQLArgumentConfigMap = [:] @@ -571,7 +577,7 @@ public struct GraphQLField { } public init( - type: GraphQLOutputType, + type: any GraphQLOutputType, description: String? = nil, deprecationReason: String? = nil, args: GraphQLArgumentConfigMap = [:], @@ -587,7 +593,7 @@ public struct GraphQLField { } public init( - type: GraphQLOutputType, + type: any GraphQLOutputType, description: String? = nil, deprecationReason: String? = nil, args: GraphQLArgumentConfigMap = [:], @@ -611,7 +617,7 @@ public typealias GraphQLFieldDefinitionMap = [String: GraphQLFieldDefinition] public final class GraphQLFieldDefinition { public let name: String public let description: String? - public internal(set) var type: GraphQLOutputType + public internal(set) var type: any GraphQLOutputType public let args: [GraphQLArgumentDefinition] public let resolve: GraphQLFieldResolve? public let subscribe: GraphQLFieldResolve? @@ -620,7 +626,7 @@ public final class GraphQLFieldDefinition { init( name: String, - type: GraphQLOutputType, + type: any GraphQLOutputType, description: String? = nil, deprecationReason: String? = nil, args: [GraphQLArgumentDefinition] = [], @@ -640,7 +646,7 @@ public final class GraphQLFieldDefinition { func replaceTypeReferences(typeMap: TypeMap) throws { let resolvedType = try resolveTypeReference(type: type, typeMap: typeMap) - guard let outputType = resolvedType as? GraphQLOutputType else { + guard let outputType = resolvedType as? (any GraphQLOutputType) else { throw GraphQLError( message: "Resolved type \"\(resolvedType)\" is not a valid output type." ) @@ -695,12 +701,12 @@ extension GraphQLFieldDefinition : KeySubscriptable { public typealias GraphQLArgumentConfigMap = [String: GraphQLArgument] public struct GraphQLArgument { - public let type: GraphQLInputType + public let type: any GraphQLInputType public let description: String? public let defaultValue: Map? public init( - type: GraphQLInputType, + type: any GraphQLInputType, description: String? = nil, defaultValue: Map? = nil ) { @@ -712,13 +718,13 @@ public struct GraphQLArgument { public struct GraphQLArgumentDefinition { public let name: String - public let type: GraphQLInputType + public let type: any GraphQLInputType public let defaultValue: Map? public let description: String? init( name: String, - type: GraphQLInputType, + type: any GraphQLInputType, defaultValue: Map? = nil, description: String? = nil ) { @@ -1365,11 +1371,11 @@ func defineInputObjectFieldMap( } public struct InputObjectField { - public let type: GraphQLInputType + public let type: any GraphQLInputType public let defaultValue: Map? public let description: String? - public init(type: GraphQLInputType, defaultValue: Map? = nil, description: String? = nil) { + public init(type: any GraphQLInputType, defaultValue: Map? = nil, description: String? = nil) { self.type = type self.defaultValue = defaultValue self.description = description @@ -1380,13 +1386,13 @@ public typealias InputObjectFieldMap = [String: InputObjectField] public final class InputObjectFieldDefinition { public let name: String - public internal(set) var type: GraphQLInputType + public internal(set) var type: any GraphQLInputType public let description: String? public let defaultValue: Map? init( name: String, - type: GraphQLInputType, + type: any GraphQLInputType, description: String? = nil, defaultValue: Map? = nil ) { @@ -1399,7 +1405,7 @@ public final class InputObjectFieldDefinition { func replaceTypeReferences(typeMap: TypeMap) throws { let resolvedType = try resolveTypeReference(type: type, typeMap: typeMap) - guard let inputType = resolvedType as? GraphQLInputType else { + guard let inputType = resolvedType as? (any GraphQLInputType) else { throw GraphQLError( message: "Resolved type \"\(resolvedType)\" is not a valid input type." ) @@ -1464,10 +1470,10 @@ public typealias InputObjectFieldDefinitionMap = [String: InputObjectFieldDefini * */ public final class GraphQLList { - public let ofType: GraphQLType + public let ofType: any GraphQLType public let kind: TypeKind = .list - public init(_ type: GraphQLType) { + public init(_ type: any GraphQLType) { self.ofType = type } @@ -1475,7 +1481,7 @@ public final class GraphQLList { self.ofType = GraphQLTypeReference(name) } - var wrappedType: GraphQLType { + var wrappedType: any GraphQLType { return ofType } @@ -1548,10 +1554,10 @@ extension GraphQLList : Hashable { * Note: the enforcement of non-nullability occurs within the executor. */ public final class GraphQLNonNull { - public let ofType: GraphQLNullableType + public let ofType: any GraphQLNullableType public let kind: TypeKind = .nonNull - public init(_ type: GraphQLNullableType) { + public init(_ type: any GraphQLNullableType) { self.ofType = type } @@ -1559,14 +1565,14 @@ public final class GraphQLNonNull { self.ofType = GraphQLTypeReference(name) } - var wrappedType: GraphQLType { + var wrappedType: any GraphQLType { return ofType } func replaceTypeReferences(typeMap: TypeMap) throws -> GraphQLNonNull { let resolvedType = try resolveTypeReference(type: ofType, typeMap: typeMap) - guard let nullableType = resolvedType as? GraphQLNullableType else { + guard let nullableType = resolvedType as? (any GraphQLNullableType) else { throw GraphQLError( message: "Resolved type \"\(resolvedType)\" is not a valid nullable type." ) diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index d748ac9a..dc78f7a3 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -10,7 +10,7 @@ let __Schema = try! GraphQLObjectType( "types": GraphQLField( type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__Type))), description: "A list of all types supported by this server.", - resolve: { schema, _, _, _ -> [GraphQLNamedType]? in + resolve: { schema, _, _, _ -> [any GraphQLNamedType]? in guard let schema = schema as? GraphQLSchema else { return nil } @@ -474,6 +474,6 @@ let TypeNameMetaFieldDef = GraphQLFieldDefinition( } ) -let introspectionTypes: [GraphQLNamedType] = [ +let introspectionTypes: [any GraphQLNamedType] = [ __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue, __EnumValue, __TypeKind ] diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index 1434671e..c0a5d64e 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -38,7 +38,7 @@ public final class GraphQLSchema { query: GraphQLObjectType, mutation: GraphQLObjectType? = nil, subscription: GraphQLObjectType? = nil, - types: [GraphQLNamedType] = [], + types: [any GraphQLNamedType] = [], directives: [GraphQLDirective] = [] ) throws { self.queryType = query @@ -49,7 +49,7 @@ public final class GraphQLSchema { self.directives = directives.isEmpty ? specifiedDirectives : directives // Build type map now to detect any errors within this schema. - var initialTypes: [GraphQLNamedType] = [ + var initialTypes: [any GraphQLNamedType] = [ queryType, ] @@ -89,11 +89,11 @@ public final class GraphQLSchema { } } - public func getType(name: String) -> GraphQLNamedType? { + public func getType(name: String) -> (any GraphQLNamedType)? { return typeMap[name] } - public func getPossibleTypes(abstractType: GraphQLAbstractType) -> [GraphQLObjectType] { + public func getPossibleTypes(abstractType: any GraphQLAbstractType) -> [GraphQLObjectType] { if let unionType = abstractType as? GraphQLUnionType { return unionType.types } @@ -113,15 +113,15 @@ public final class GraphQLSchema { // @deprecated: use isSubType instead - will be removed in the future. public func isPossibleType( - abstractType: GraphQLAbstractType, + abstractType: any GraphQLAbstractType, possibleType: GraphQLObjectType ) throws -> Bool { isSubType(abstractType: abstractType, maybeSubType: possibleType) } public func isSubType( - abstractType: GraphQLCompositeType, - maybeSubType: GraphQLNamedType + abstractType: any GraphQLCompositeType, + maybeSubType: any GraphQLNamedType ) -> Bool { var map = subTypeMap[abstractType.name] @@ -175,7 +175,7 @@ extension GraphQLSchema : Encodable { } } -public typealias TypeMap = [String: GraphQLNamedType] +public typealias TypeMap = [String: any GraphQLNamedType] public struct InterfaceImplementations { public let objects: [GraphQLObjectType] @@ -191,7 +191,7 @@ public struct InterfaceImplementations { } func collectImplementations( - types: [GraphQLNamedType] + types: [any GraphQLNamedType] ) -> [String : InterfaceImplementations] { var implementations: [String: InterfaceImplementations] = [:] @@ -219,14 +219,14 @@ func collectImplementations( return implementations } -func typeMapReducer(typeMap: TypeMap, type: GraphQLType) throws -> TypeMap { +func typeMapReducer(typeMap: TypeMap, type: any GraphQLType) throws -> TypeMap { var typeMap = typeMap - if let type = type as? GraphQLWrapperType { + if let type = type as? (any GraphQLWrapperType) { return try typeMapReducer(typeMap: typeMap, type: type.wrappedType) } - guard let type = type as? GraphQLNamedType else { + guard let type = type as? (any GraphQLNamedType) else { return typeMap // Should never happen } @@ -353,13 +353,13 @@ func assert( func replaceTypeReferences(typeMap: TypeMap) throws { for type in typeMap { - if let typeReferenceContainer = type.value as? GraphQLTypeReferenceContainer { + if let typeReferenceContainer = type.value as? (any GraphQLTypeReferenceContainer) { try typeReferenceContainer.replaceTypeReferences(typeMap: typeMap) } } } -func resolveTypeReference(type: GraphQLType, typeMap: TypeMap) throws -> GraphQLType { +func resolveTypeReference(type: any GraphQLType, typeMap: TypeMap) throws -> any GraphQLType { if let type = type as? GraphQLTypeReference { guard let resolvedType = typeMap[type.name] else { throw GraphQLError( @@ -381,8 +381,8 @@ func resolveTypeReference(type: GraphQLType, typeMap: TypeMap) throws -> GraphQL return type } -func resolveTypeReferences(types: [GraphQLType], typeMap: TypeMap) throws -> [GraphQLType] { - var resolvedTypes: [GraphQLType] = [] +func resolveTypeReferences(types: [any GraphQLType], typeMap: TypeMap) throws -> [any GraphQLType] { + var resolvedTypes: [any GraphQLType] = [] for type in types { try resolvedTypes.append(resolveTypeReference(type: type, typeMap: typeMap)) diff --git a/Sources/GraphQL/Utilities/ASTFromValue.swift b/Sources/GraphQL/Utilities/ASTFromValue.swift index 735cee64..5f3eeb60 100644 --- a/Sources/GraphQL/Utilities/ASTFromValue.swift +++ b/Sources/GraphQL/Utilities/ASTFromValue.swift @@ -18,12 +18,12 @@ import Foundation */ func astFromValue( value: Map, - type: GraphQLInputType + type: any GraphQLInputType ) throws -> Value? { if let type = type as? GraphQLNonNull { // Note: we're not checking that the result is non-null. // This function is not responsible for validating the input value. - return try astFromValue(value: value, type: type.ofType as! GraphQLInputType) + return try astFromValue(value: value, type: type.ofType as! (any GraphQLInputType)) } guard value != .null else { @@ -33,7 +33,7 @@ func astFromValue( // Convert array to GraphQL list. If the GraphQLType is a list, but // the value is not an array, convert the value using the list's item type. if let type = type as? GraphQLList { - let itemType = type.ofType as! GraphQLInputType + let itemType = type.ofType as! (any GraphQLInputType) if case .array(let value) = value { var valuesASTs: [Value] = [] @@ -72,7 +72,7 @@ func astFromValue( return .objectValue(ObjectValue(fields: fieldASTs)) } - guard let leafType = type as? GraphQLLeafType else { + guard let leafType = type as? (any GraphQLLeafType) else { throw GraphQLError( message: "Must provide Input Type, cannot use: \(type)" ) diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index 73c0b5c1..c33a2668 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -3,8 +3,8 @@ enum BuildClientSchemaError: Error { } public func buildClientSchema(introspection: IntrospectionQuery) throws -> GraphQLSchema { let schemaIntrospection = introspection.__schema - var typeMap = [String: GraphQLNamedType]() - typeMap = try schemaIntrospection.types.reduce(into: [String: GraphQLNamedType]()) { + var typeMap = [String: any GraphQLNamedType]() + typeMap = try schemaIntrospection.types.reduce(into: [String: any GraphQLNamedType]()) { $0[$1.x.name] = try buildType($1.x) } @@ -16,7 +16,7 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph } } - func getNamedType(name: String) throws -> GraphQLNamedType { + func getNamedType(name: String) throws -> any GraphQLNamedType { guard let type = typeMap[name] else { throw BuildClientSchemaError.invalid("Couldn't find type named \(name)") } @@ -41,12 +41,12 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph return type } - func getType(_ typeRef: IntrospectionTypeRef) throws -> GraphQLType { + func getType(_ typeRef: IntrospectionTypeRef) throws -> any GraphQLType { switch typeRef { case .list(let ofType): return GraphQLList(try getType(ofType)) case .nonNull(let ofType): - guard let type = try getType(ofType) as? GraphQLNullableType else { + guard let type = try getType(ofType) as? (any GraphQLNullableType) else { throw BuildClientSchemaError.invalid("Expected nullable type") } return GraphQLNonNull(type) @@ -77,7 +77,7 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph } func buildInputValue(inputValue: IntrospectionInputValue) throws -> GraphQLArgument { - guard let type = try getType(inputValue.type) as? GraphQLInputType else { + guard let type = try getType(inputValue.type) as? (any GraphQLInputType) else { throw BuildClientSchemaError.invalid("Introspection must provide input type for arguments") } let defaultValue = try inputValue.defaultValue.map { @@ -86,7 +86,7 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph return GraphQLArgument(type: type, description: inputValue.description, defaultValue: defaultValue) } - func buildType(_ type: IntrospectionType) throws -> GraphQLNamedType { + func buildType(_ type: IntrospectionType) throws -> any GraphQLNamedType { switch type { case let type as IntrospectionScalarType: return try GraphQLScalarType( diff --git a/Sources/GraphQL/Utilities/IsValidValue.swift b/Sources/GraphQL/Utilities/IsValidValue.swift index a7a30395..fe269daf 100644 --- a/Sources/GraphQL/Utilities/IsValidValue.swift +++ b/Sources/GraphQL/Utilities/IsValidValue.swift @@ -3,10 +3,10 @@ * accepted for that type. This is primarily useful for validating the * runtime values of query variables. */ -func validate(value: Map, forType type: GraphQLInputType) throws -> [String] { +func validate(value: Map, forType type: any GraphQLInputType) throws -> [String] { // A value must be provided if the type is non-null. if let nonNullType = type as? GraphQLNonNull { - guard let wrappedType = nonNullType.ofType as? GraphQLInputType else { + guard let wrappedType = nonNullType.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "Input non-null type must wrap another input type") } @@ -27,7 +27,7 @@ func validate(value: Map, forType type: GraphQLInputType) throws -> [String] { // Lists accept a non-list value as a list of one. if let listType = type as? GraphQLList { - guard let itemType = listType.ofType as? GraphQLInputType else { + guard let itemType = listType.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "Input list type must wrap another input type") } @@ -75,7 +75,7 @@ func validate(value: Map, forType type: GraphQLInputType) throws -> [String] { return errors } - if let leafType = type as? GraphQLLeafType { + if let leafType = type as? (any GraphQLLeafType) { // Scalar/Enum input checks to ensure the type can parse the value to // a non-null value. do { diff --git a/Sources/GraphQL/Utilities/TypeComparators.swift b/Sources/GraphQL/Utilities/TypeComparators.swift index dd05be3b..bbd8787f 100644 --- a/Sources/GraphQL/Utilities/TypeComparators.swift +++ b/Sources/GraphQL/Utilities/TypeComparators.swift @@ -1,7 +1,7 @@ /** * Provided two types, return true if the types are equal (invariant). */ -public func isEqualType(_ typeA: GraphQLType, _ typeB: GraphQLType) -> Bool { +public func isEqualType(_ typeA: any GraphQLType, _ typeB: any GraphQLType) -> Bool { // Equivalent types are equal. if typeA == typeB { return true @@ -21,7 +21,7 @@ public func isEqualType(_ typeA: GraphQLType, _ typeB: GraphQLType) -> Bool { return false } -func == (lhs: GraphQLType, rhs: GraphQLType) -> Bool { +func == (lhs: any GraphQLType, rhs: any GraphQLType) -> Bool { switch lhs { case let l as GraphQLScalarType: if let r = rhs as? GraphQLScalarType { @@ -72,8 +72,8 @@ func == (lhs: GraphQLType, rhs: GraphQLType) -> Bool { */ public func isTypeSubTypeOf( _ schema: GraphQLSchema, - _ maybeSubType: GraphQLType, - _ superType: GraphQLType + _ maybeSubType: any GraphQLType, + _ superType: any GraphQLType ) throws -> Bool { // Equivalent type is a valid subtype if maybeSubType == superType { @@ -106,7 +106,7 @@ public func isTypeSubTypeOf( // If superType type is an abstract type, check if it is super type of maybeSubType. if - let superType = superType as? GraphQLAbstractType, + let superType = superType as? (any GraphQLAbstractType), let maybeSubType = maybeSubType as? GraphQLObjectType, schema.isSubType( abstractType: superType, @@ -117,7 +117,7 @@ public func isTypeSubTypeOf( } if - let superType = superType as? GraphQLAbstractType, + let superType = superType as? (any GraphQLAbstractType), let maybeSubType = maybeSubType as? GraphQLInterfaceType, schema.isSubType( abstractType: superType, @@ -142,16 +142,16 @@ public func isTypeSubTypeOf( */ func doTypesOverlap( schema: GraphQLSchema, - typeA: GraphQLCompositeType, - typeB: GraphQLCompositeType + typeA: any GraphQLCompositeType, + typeB: any GraphQLCompositeType ) -> Bool { // Equivalent types overlap if typeA == typeB { return true } - if let typeA = typeA as? GraphQLAbstractType { - if let typeB = typeB as? GraphQLAbstractType { + if let typeA = typeA as? (any GraphQLAbstractType) { + if let typeB = typeB as? (any GraphQLAbstractType) { // If both types are abstract, then determine if there is any intersection // between possible concrete types of each. return schema.getPossibleTypes(abstractType: typeA).contains { typeA in @@ -171,7 +171,7 @@ func doTypesOverlap( } } - if let typeB = typeB as? GraphQLAbstractType { + if let typeB = typeB as? (any GraphQLAbstractType) { // Determine if the former type is a possible concrete type of the latter. return schema.isSubType( abstractType: typeB, diff --git a/Sources/GraphQL/Utilities/TypeFromAST.swift b/Sources/GraphQL/Utilities/TypeFromAST.swift index a187f207..9bd228fb 100644 --- a/Sources/GraphQL/Utilities/TypeFromAST.swift +++ b/Sources/GraphQL/Utilities/TypeFromAST.swift @@ -1,4 +1,4 @@ -public func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> GraphQLType? { +public func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> (any GraphQLType)? { switch inputTypeAST { case let .listType(listType): if let innerType = typeFromAST(schema: schema, inputTypeAST: listType.type) { @@ -6,7 +6,7 @@ public func typeFromAST(schema: GraphQLSchema, inputTypeAST: Type) -> GraphQLTyp } case let .nonNullType(nonNullType): if let innerType = typeFromAST(schema: schema, inputTypeAST: nonNullType.type) { - return GraphQLNonNull(innerType as! GraphQLNullableType) + return GraphQLNonNull(innerType as! (any GraphQLNullableType)) } case let .namedType(namedType): return schema.getType(name: namedType.name.value) diff --git a/Sources/GraphQL/Utilities/TypeInfo.swift b/Sources/GraphQL/Utilities/TypeInfo.swift index a1bb28a6..f11738bb 100644 --- a/Sources/GraphQL/Utilities/TypeInfo.swift +++ b/Sources/GraphQL/Utilities/TypeInfo.swift @@ -5,9 +5,9 @@ */ public final class TypeInfo { let schema: GraphQLSchema; - var typeStack: [GraphQLOutputType?] - var parentTypeStack: [GraphQLCompositeType?] - var inputTypeStack: [GraphQLInputType?] + var typeStack: [(any GraphQLOutputType)?] + var parentTypeStack: [(any GraphQLCompositeType)?] + var inputTypeStack: [(any GraphQLInputType)?] var fieldDefStack: [GraphQLFieldDefinition?] var directive: GraphQLDirective? var argument: GraphQLArgumentDefinition? @@ -22,21 +22,21 @@ public final class TypeInfo { self.argument = nil } - public var type: GraphQLOutputType? { + public var type: (any GraphQLOutputType)? { if !typeStack.isEmpty { return typeStack[typeStack.count - 1] } return nil } - public var parentType: GraphQLCompositeType? { + public var parentType: (any GraphQLCompositeType)? { if !parentTypeStack.isEmpty { return parentTypeStack[parentTypeStack.count - 1] } return nil } - public var inputType: GraphQLInputType? { + public var inputType: (any GraphQLInputType)? { if !inputTypeStack.isEmpty { return inputTypeStack[inputTypeStack.count - 1] } @@ -54,9 +54,9 @@ public final class TypeInfo { switch node { case is SelectionSet: let namedType = getNamedType(type: type) - var compositeType: GraphQLCompositeType? = nil + var compositeType: (any GraphQLCompositeType)? = nil - if let type = namedType as? GraphQLCompositeType { + if let type = namedType as? (any GraphQLCompositeType) { compositeType = type } @@ -76,7 +76,7 @@ public final class TypeInfo { directive = schema.getDirective(name: node.name.value) case let node as OperationDefinition: - var type: GraphQLOutputType? = nil + var type: (any GraphQLOutputType)? = nil switch node.operation { case .query: @@ -92,18 +92,18 @@ public final class TypeInfo { case let node as InlineFragment: let typeConditionAST = node.typeCondition let outputType = typeConditionAST != nil ? typeFromAST(schema: schema, inputTypeAST: .namedType(typeConditionAST!)) : self.type - typeStack.append(outputType as? GraphQLOutputType) + typeStack.append(outputType as? (any GraphQLOutputType)) case let node as FragmentDefinition: let outputType = typeFromAST(schema: schema, inputTypeAST: .namedType(node.typeCondition)) - typeStack.append(outputType as? GraphQLOutputType) + typeStack.append(outputType as? (any GraphQLOutputType)) case let node as VariableDefinition: let inputType = typeFromAST(schema: schema, inputTypeAST: node.type) - inputTypeStack.append(inputType as? GraphQLInputType) + inputTypeStack.append(inputType as? (any GraphQLInputType)) case let node as Argument: - var argType: GraphQLInputType? = nil + var argType: (any GraphQLInputType)? = nil if let directive = self.directive { if let argDef = directive.args.find({ $0.name == node.name.value }) { @@ -121,7 +121,7 @@ public final class TypeInfo { case is ListType: // could be ListValue if let listType = getNullableType(type: self.inputType) as? GraphQLList { - inputTypeStack.append(listType.ofType as? GraphQLInputType) + inputTypeStack.append(listType.ofType as? (any GraphQLInputType)) } inputTypeStack.append(nil) @@ -173,10 +173,10 @@ public final class TypeInfo { * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. */ -public func getFieldDef(schema: GraphQLSchema, parentType: GraphQLType, fieldAST: Field) -> GraphQLFieldDefinition? { +public func getFieldDef(schema: GraphQLSchema, parentType: any GraphQLType, fieldAST: Field) -> GraphQLFieldDefinition? { let name = fieldAST.name.value - if let parentType = parentType as? GraphQLNamedType { + if let parentType = parentType as? (any GraphQLNamedType) { if name == SchemaMetaFieldDef.name && schema.queryType.name == parentType.name { return SchemaMetaFieldDef } diff --git a/Sources/GraphQL/Utilities/ValueFromAST.swift b/Sources/GraphQL/Utilities/ValueFromAST.swift index e37a0d7c..d20305be 100644 --- a/Sources/GraphQL/Utilities/ValueFromAST.swift +++ b/Sources/GraphQL/Utilities/ValueFromAST.swift @@ -17,12 +17,12 @@ import OrderedCollections * | Enum Value | .string | * */ -func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: Map] = [:]) throws -> Map { +func valueFromAST(valueAST: Value, type: any GraphQLInputType, variables: [String: Map] = [:]) throws -> Map { if let nonNull = type as? GraphQLNonNull { // Note: we're not checking that the result of valueFromAST is non-null. // We're assuming that this query has been validated and the value used // here is of the correct type. - guard let nonNullType = nonNull.ofType as? GraphQLInputType else { + guard let nonNullType = nonNull.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "NonNull must wrap an input type") } return try valueFromAST(valueAST: valueAST, type: nonNullType, variables: variables) @@ -45,7 +45,7 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M } if let list = type as? GraphQLList { - guard let itemType = list.ofType as? GraphQLInputType else { + guard let itemType = list.ofType as? (any GraphQLInputType) else { throw GraphQLError(message: "Input list must wrap an input type") } @@ -98,7 +98,7 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M return .dictionary(object) } - if let leafType = type as? GraphQLLeafType { + if let leafType = type as? (any GraphQLLeafType) { return try leafType.parseLiteral(valueAST: valueAST) } diff --git a/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift b/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift index 4a71de5c..81426d29 100644 --- a/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift +++ b/Sources/GraphQL/Validation/Rules/FieldsOnCorrectTypeRule.swift @@ -72,10 +72,10 @@ struct FieldsOnCorrectTypeRule: ValidationRule { */ func getSuggestedTypeNames( schema: GraphQLSchema, - type: GraphQLOutputType, + type: any GraphQLOutputType, fieldName: String ) -> [String] { - if let type = type as? GraphQLAbstractType { + if let type = type as? (any GraphQLAbstractType) { var suggestedObjectTypes: [String] = [] var interfaceUsageCount: [String: Int] = [:] @@ -116,7 +116,7 @@ func getSuggestedTypeNames( */ func getSuggestedFieldNames( schema: GraphQLSchema, - type: GraphQLOutputType, + type: any GraphQLOutputType, fieldName: String ) -> [String] { if let type = type as? GraphQLObjectType { diff --git a/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift b/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift index 12150026..aade7393 100644 --- a/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift +++ b/Sources/GraphQL/Validation/Rules/PossibleFragmentSpreadsRule.swift @@ -10,7 +10,7 @@ struct PossibleFragmentSpreadsRule: ValidationRule { func enter(inlineFragment: InlineFragment, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { guard - let fragType = context.type as? GraphQLCompositeType, + let fragType = context.type as? (any GraphQLCompositeType), let parentType = context.parentType else { return .continue @@ -69,14 +69,14 @@ struct PossibleFragmentSpreadsRule: ValidationRule { func getFragmentType( context: ValidationContext, name: String -) -> GraphQLCompositeType? { +) -> (any GraphQLCompositeType)? { if let fragment = context.getFragment(name: name) { let type = typeFromAST( schema: context.schema, inputTypeAST: .namedType(fragment.typeCondition) ) - if let type = type as? GraphQLCompositeType { + if let type = type as? (any GraphQLCompositeType) { return type } } diff --git a/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift b/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift index 27feb673..9de69a96 100644 --- a/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift +++ b/Sources/GraphQL/Validation/Rules/ScalarLeafsRule.swift @@ -1,9 +1,9 @@ -func noSubselectionAllowedMessage(fieldName: String, type: GraphQLType) -> String { +func noSubselectionAllowedMessage(fieldName: String, type: any GraphQLType) -> String { return "Field \"\(fieldName)\" must not have a selection since " + "type \"\(type)\" has no subfields." } -func requiredSubselectionMessage(fieldName: String, type: GraphQLType) -> String { +func requiredSubselectionMessage(fieldName: String, type: any GraphQLType) -> String { return "Field \"\(fieldName)\" of type \"\(type)\" must have a " + "selection of subfields. Did you mean \"\(fieldName) { ... }\"?" } diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index f4c8b5fe..e28e14bf 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -78,7 +78,7 @@ extension OperationDefinition: HasSelectionSet { } extension FragmentDefinition: HasSelectionSet { } -typealias VariableUsage = (node: Variable, type: GraphQLInputType?) +typealias VariableUsage = (node: Variable, type: (any GraphQLInputType)?) /** * An instance of this class is passed as the "this" context to all validators, @@ -207,15 +207,15 @@ final class ValidationContext { return usages } - var type: GraphQLOutputType? { + var type: (any GraphQLOutputType)? { return typeInfo.type } - var parentType: GraphQLCompositeType? { + var parentType: (any GraphQLCompositeType)? { return typeInfo.parentType } - var inputType: GraphQLInputType? { + var inputType: (any GraphQLInputType)? { return typeInfo.inputType } From edcfcbac71de7ab94c9b3b504e1686dc7e77fd96 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Thu, 9 Jun 2022 15:20:47 +0100 Subject: [PATCH 12/19] Make GraphQLNamedType conform to Hashable --- Sources/GraphQL/Type/Definition.swift | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index f58244a0..094fabcf 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -4,7 +4,7 @@ import NIO /** * These are all of the possible kinds of types. */ -public protocol GraphQLType : CustomDebugStringConvertible, Encodable, KeySubscriptable, AnyObject, Equatable {} +public protocol GraphQLType : CustomDebugStringConvertible, Encodable, KeySubscriptable, AnyObject {} extension GraphQLScalarType : GraphQLType {} extension GraphQLObjectType : GraphQLType {} extension GraphQLInterfaceType : GraphQLType {} @@ -14,12 +14,6 @@ extension GraphQLInputObjectType : GraphQLType {} extension GraphQLList : GraphQLType {} extension GraphQLNonNull : GraphQLType {} -extension GraphQLType { - public static func == (lhs: Self, rhs: Self) -> Bool { - ObjectIdentifier(lhs) == ObjectIdentifier(rhs) - } -} - /** * These types may be used as input types for arguments and directives. */ @@ -120,10 +114,20 @@ func getNullableType(type: (any GraphQLType)?) -> (any GraphQLNullableType)? { /** * These named types do not include modifiers like List or NonNull. */ -public protocol GraphQLNamedType : GraphQLNullableType { +public protocol GraphQLNamedType : GraphQLNullableType, Hashable { var name: String { get } } +extension GraphQLNamedType { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + } +} + extension GraphQLScalarType : GraphQLNamedType {} extension GraphQLObjectType : GraphQLNamedType {} extension GraphQLInterfaceType : GraphQLNamedType {} From fb0397c1fc03199a07910080b71f77a62b24ab0e Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 10 Jun 2022 21:19:55 +0100 Subject: [PATCH 13/19] Add KnownFragmentNamesRule validation rule --- .../Rules/KnownFragmentNamesRule.swift | 26 +++++++++++++++++++ .../GraphQL/Validation/SpecifiedRules.swift | 2 +- .../KnownFragmentNamesRuleTests.swift | 22 ++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Sources/GraphQL/Validation/Rules/KnownFragmentNamesRule.swift create mode 100644 Tests/GraphQLTests/ValidationTests/KnownFragmentNamesRuleTests.swift diff --git a/Sources/GraphQL/Validation/Rules/KnownFragmentNamesRule.swift b/Sources/GraphQL/Validation/Rules/KnownFragmentNamesRule.swift new file mode 100644 index 00000000..e3754841 --- /dev/null +++ b/Sources/GraphQL/Validation/Rules/KnownFragmentNamesRule.swift @@ -0,0 +1,26 @@ +import Foundation + +/** + * Known fragment names + * + * A GraphQL document is only valid if all `...Fragment` fragment spreads refer + * to fragments defined in the same document. + * + * See https://spec.graphql.org/draft/#sec-Fragment-spread-target-defined + */ +struct KnownFragmentNamesRule: ValidationRule { + let context: ValidationContext + + func enter(fragmentSpread: FragmentSpread, key: AnyKeyPath?, parent: VisitorParent?, ancestors: [VisitorParent]) -> VisitResult { + let fragmentName = fragmentSpread.name.value + if context.getFragment(name: fragmentName) == nil { + context.report( + error: GraphQLError( + message: "Unknown fragment \(fragmentName)", + nodes: [fragmentSpread.name] + ) + ) + } + return .continue + } +} diff --git a/Sources/GraphQL/Validation/SpecifiedRules.swift b/Sources/GraphQL/Validation/SpecifiedRules.swift index aa9a2d8f..e20e05d0 100644 --- a/Sources/GraphQL/Validation/SpecifiedRules.swift +++ b/Sources/GraphQL/Validation/SpecifiedRules.swift @@ -10,7 +10,7 @@ let specifiedRules: [ValidationRule.Type] = [ ScalarLeafsRule.self, FieldsOnCorrectTypeRule.self, // UniqueFragmentNames, - // KnownFragmentNames, + KnownFragmentNamesRule.self, // NoUnusedFragments, PossibleFragmentSpreadsRule.self, // NoFragmentCycles, diff --git a/Tests/GraphQLTests/ValidationTests/KnownFragmentNamesRuleTests.swift b/Tests/GraphQLTests/ValidationTests/KnownFragmentNamesRuleTests.swift new file mode 100644 index 00000000..2069cd24 --- /dev/null +++ b/Tests/GraphQLTests/ValidationTests/KnownFragmentNamesRuleTests.swift @@ -0,0 +1,22 @@ +@testable import GraphQL +import XCTest + +class KnownFragmentNamesRuleTests : ValidationTestCase { + override func setUp() { + rule = KnownFragmentNamesRule.self + } + + func testValidWithKnownFragmentName() throws { + try assertValid(""" + fragment f on Dog { name } + query { dog { ...f } } + """) + } + + func testInvalidWithUnknownFragmentName() throws { + try assertInvalid( + errorCount: 1, + query: "{ dog { ...f } }" + ) + } +} From 215d0fa659f51f721a78a8c5dede1b7c7909c971 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 22 Jul 2022 00:04:43 +0100 Subject: [PATCH 14/19] Use ordered dictionary for GraphQLEnumValueMap --- Sources/GraphQL/Type/Definition.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 094fabcf..56a7be5c 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1,5 +1,6 @@ import Foundation import NIO +import OrderedCollections /** * These are all of the possible kinds of types. @@ -1205,7 +1206,7 @@ func defineEnumValues( return definitions } -public typealias GraphQLEnumValueMap = [String: GraphQLEnumValue] +public typealias GraphQLEnumValueMap = OrderedDictionary public struct GraphQLEnumValue { public let value: Map From de70703d477fe3995dcfd5ecee363bcda2a9990c Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 29 Jul 2022 11:44:03 +0100 Subject: [PATCH 15/19] Handle InputObjects in BuildClientSchema for introspection queries --- .../GraphQL/Utilities/BuildClientSchema.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index c33a2668..efda9ec4 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -86,6 +86,18 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph return GraphQLArgument(type: type, description: inputValue.description, defaultValue: defaultValue) } + func buildInputObjectFieldMap(args: [IntrospectionInputValue]) throws -> InputObjectFieldMap { + try args.reduce(into: [:]) { acc, inputValue in + guard let type = try getType(inputValue.type) as? (any GraphQLInputType) else { + throw BuildClientSchemaError.invalid("Introspection must provide input type for arguments") + } + let defaultValue = try inputValue.defaultValue.map { + try valueFromAST(valueAST: parseValue(source: $0), type: type) + } + acc[inputValue.name] = InputObjectField(type: type, defaultValue: defaultValue, description: inputValue.description) + } + } + func buildType(_ type: IntrospectionType) throws -> any GraphQLNamedType { switch type { case let type as IntrospectionScalarType: @@ -128,10 +140,10 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph ) } ) -// case .scalar: -// return GraphQLScalarType(name: type["name"].string!, description: type["description"].string) -// case .object: -// return GraphQLObjectType(name: type["name"].string!, description: type["description"].string, fields: buildFieldDefMap(type: type), interfaces: buildImplementationsList(type)) + case let type as IntrospectionInputObjectType: + return try GraphQLInputObjectType(name: type.name, + description: type.description, + fields: buildInputObjectFieldMap(args: type.inputFields)) default: fatalError() } From 1e830cd961a2e3cda2a265ca37902d187b983d70 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 29 Jul 2022 12:00:15 +0100 Subject: [PATCH 16/19] Make InputObjectFieldMap an ordered dictionary --- Sources/GraphQL/Type/Definition.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 56a7be5c..cc59d224 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1387,7 +1387,7 @@ public struct InputObjectField { } } -public typealias InputObjectFieldMap = [String: InputObjectField] +public typealias InputObjectFieldMap = OrderedDictionary public final class InputObjectFieldDefinition { public let name: String From 3a4e165f8254da856f0183d91961dae720ae2486 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 29 Jul 2022 12:19:12 +0100 Subject: [PATCH 17/19] Make InputObject fields lazy --- Sources/GraphQL/Execution/Execute.swift | 4 +-- Sources/GraphQL/Type/Definition.swift | 31 ++++++++++++++---- Sources/GraphQL/Type/Introspection.swift | 2 +- .../GraphQL/Utilities/BuildClientSchema.swift | 8 +++-- .../TypeTests/DecodableTests.swift | 32 ------------------- 5 files changed, 33 insertions(+), 44 deletions(-) delete mode 100644 Tests/GraphQLTests/TypeTests/DecodableTests.swift diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 97e90e1c..8efa2fd3 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -912,13 +912,13 @@ func completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning .null if serialization is not possible. - if let returnType = returnType as? GraphQLLeafType { + if let returnType = returnType as? any GraphQLLeafType { return exeContext.eventLoopGroup.next().makeSucceededFuture(try completeLeafValue(returnType: returnType, result: r)) } // If field type is an abstract type, Interface or Union, determine the // runtime Object type and complete for that type. - if let returnType = returnType as? GraphQLAbstractType { + if let returnType = returnType as? any GraphQLAbstractType { return try completeAbstractValue( exeContext: exeContext, returnType: returnType, diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index cc59d224..375f4abe 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1279,21 +1279,32 @@ extension GraphQLEnumValueDefinition : KeySubscriptable { public final class GraphQLInputObjectType { public let name: String public let description: String? - public let fields: InputObjectFieldDefinitionMap + public let fieldsThunk: () -> InputObjectFieldMap + public lazy var fields: InputObjectFieldDefinitionMap = { + try! defineInputObjectFieldMap( + name: name, + fields: fieldsThunk() + ) + }() public let kind: TypeKind = .inputObject - public init( + public convenience init( name: String, description: String? = nil, fields: InputObjectFieldMap = [:] + ) throws { + try self.init(name: name, description: description, fields: { fields }) + } + + public init( + name: String, + description: String? = nil, + fields: @escaping () -> InputObjectFieldMap ) throws { try assertValid(name: name) self.name = name self.description = description - self.fields = try defineInputObjectFieldMap( - name: name, - fields: fields - ) + self.fieldsThunk = fields } func replaceTypeReferences(typeMap: TypeMap) throws { @@ -1310,6 +1321,14 @@ extension GraphQLInputObjectType : Encodable { case fields case kind } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(fields, forKey: .fields) + try container.encode(kind, forKey: .kind) + } } extension GraphQLInputObjectType : KeySubscriptable { diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index dc78f7a3..d8290bac 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -265,7 +265,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType( "possibleTypes": GraphQLField( type: GraphQLList(GraphQLNonNull(GraphQLTypeReference("__Type"))), resolve: { type, args, _, info -> [GraphQLObjectType]? in - guard let type = type as? GraphQLAbstractType else { + guard let type = type as? any GraphQLAbstractType else { return nil } diff --git a/Sources/GraphQL/Utilities/BuildClientSchema.swift b/Sources/GraphQL/Utilities/BuildClientSchema.swift index efda9ec4..9452da66 100644 --- a/Sources/GraphQL/Utilities/BuildClientSchema.swift +++ b/Sources/GraphQL/Utilities/BuildClientSchema.swift @@ -141,9 +141,11 @@ public func buildClientSchema(introspection: IntrospectionQuery) throws -> Graph } ) case let type as IntrospectionInputObjectType: - return try GraphQLInputObjectType(name: type.name, - description: type.description, - fields: buildInputObjectFieldMap(args: type.inputFields)) + return try GraphQLInputObjectType( + name: type.name, + description: type.description, + fields: { try! buildInputObjectFieldMap(args: type.inputFields) } + ) default: fatalError() } diff --git a/Tests/GraphQLTests/TypeTests/DecodableTests.swift b/Tests/GraphQLTests/TypeTests/DecodableTests.swift deleted file mode 100644 index 20533f72..00000000 --- a/Tests/GraphQLTests/TypeTests/DecodableTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -@testable import GraphQL -import XCTest - -class DecodableTests: XCTestCase { - - func testDecodeObjectType() { - let decoder = JSONDecoder() - -// let encoder = JSONEncoder() -// let data = try! encoder.encode(IntrospectionType.scalar(name: "asdf", description: "foo", specifiedByURL: "bar")) -// print(String(data: data, encoding: .utf8)) -// let x = try! decoder.decode(IntrospectionType.self, from: data) -// print(x) - - - let x = try! decoder.decode(AnyIntrospectionType.self, from: """ - { - "kind": "OBJECT", - "name": "Foo" - } - """.data(using: .utf8)!) - print(x) - - - let schemaData = try! Data(contentsOf: URL(fileURLWithPath: "/Users/luke/Desktop/minm-schema.json")) - let z = try! decoder.decode(IntrospectionQuery.self, from: schemaData) - print(z) - let schema = try! buildClientSchema(introspection: z) - print(schema) - } - -} From d476ab06ee8acb70142e185b29c8daa0feecc14b Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Fri, 29 Jul 2022 12:41:47 +0100 Subject: [PATCH 18/19] Add tests for VariablesAreInputTypesRule --- .../ValidationTests/ExampleSchema.swift | 13 ++++++++++++ .../VariablesAreInputTypesRuleTests.swift | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 Tests/GraphQLTests/ValidationTests/VariablesAreInputTypesRuleTests.swift diff --git a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift index e507be6e..2a2c2e0a 100644 --- a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift +++ b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift @@ -377,6 +377,18 @@ let ValidationExampleHumanOrAlien = try! GraphQLUnionType( types: [ValidationExampleHuman, ValidationExampleAlien] ) +// input Treat { +// name: String! +// amount: Int! +// } +let ValidationExampleTreat = try! GraphQLInputObjectType( + name: "Treat", + fields: [ + "name": InputObjectField(type: GraphQLNonNull(GraphQLString)), + "amount": InputObjectField(type: GraphQLNonNull(GraphQLInt)) + ] +) + // type QueryRoot { // dog: Dog // } @@ -402,5 +414,6 @@ let ValidationExampleSchema = try! GraphQLSchema( ValidationExampleDog, ValidationExampleHuman, ValidationExampleAlien, + ValidationExampleTreat ] ) diff --git a/Tests/GraphQLTests/ValidationTests/VariablesAreInputTypesRuleTests.swift b/Tests/GraphQLTests/ValidationTests/VariablesAreInputTypesRuleTests.swift new file mode 100644 index 00000000..f58b5589 --- /dev/null +++ b/Tests/GraphQLTests/ValidationTests/VariablesAreInputTypesRuleTests.swift @@ -0,0 +1,20 @@ +@testable import GraphQL +import XCTest + +class VariablesAreInputTypesRuleTests : ValidationTestCase { + override func setUp() { + rule = VariablesAreInputTypesRule.self + } + + func testValidWithInputObject() throws { + try assertValid( + "query ($treat: Treat) { dog { __typename } } " + ) + } + + func testInvalidWithObject() throws { + try assertInvalid(errorCount: 1, query: + "query ($dog: Dog) { dog { __typename } } " + ) + } +} From a3530565b58b1688abddd868ddf053eb8fb54f40 Mon Sep 17 00:00:00 2001 From: Luke Lau Date: Mon, 1 Aug 2022 13:10:28 +0100 Subject: [PATCH 19/19] Use OrderedDictionary throughout schema definitions --- Sources/GraphQL/Type/Definition.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 375f4abe..2bb8f0d5 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -557,7 +557,7 @@ public struct GraphQLResolveInfo { public let variableValues: [String: Any] } -public typealias GraphQLFieldMap = [String: GraphQLField] +public typealias GraphQLFieldMap = OrderedDictionary public struct GraphQLField { public let type: any GraphQLOutputType @@ -617,7 +617,7 @@ public struct GraphQLField { } } -public typealias GraphQLFieldDefinitionMap = [String: GraphQLFieldDefinition] +public typealias GraphQLFieldDefinitionMap = OrderedDictionary public final class GraphQLFieldDefinition { public let name: String @@ -703,7 +703,7 @@ extension GraphQLFieldDefinition : KeySubscriptable { } } -public typealias GraphQLArgumentConfigMap = [String: GraphQLArgument] +public typealias GraphQLArgumentConfigMap = OrderedDictionary public struct GraphQLArgument { public let type: any GraphQLInputType @@ -1473,7 +1473,7 @@ extension InputObjectFieldDefinition : KeySubscriptable { } } -public typealias InputObjectFieldDefinitionMap = [String: InputObjectFieldDefinition] +public typealias InputObjectFieldDefinitionMap = OrderedDictionary /** * List Modifier