graphql 0.9.5 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/base_type.rb +24 -0
- data/lib/graphql/definition_helpers/defined_by_config.rb +5 -4
- data/lib/graphql/definition_helpers/non_null_with_bang.rb +1 -1
- data/lib/graphql/definition_helpers/type_definer.rb +1 -1
- data/lib/graphql/enum_type.rb +16 -3
- data/lib/graphql/input_object_type.rb +10 -0
- data/lib/graphql/language.rb +0 -5
- data/lib/graphql/language/nodes.rb +79 -75
- data/lib/graphql/language/parser.rb +109 -106
- data/lib/graphql/language/transform.rb +100 -91
- data/lib/graphql/language/visitor.rb +78 -74
- data/lib/graphql/list_type.rb +5 -0
- data/lib/graphql/non_null_type.rb +6 -2
- data/lib/graphql/query.rb +58 -9
- data/lib/graphql/query/arguments.rb +29 -26
- data/lib/graphql/query/base_execution/value_resolution.rb +3 -3
- data/lib/graphql/query/directive_chain.rb +1 -1
- data/lib/graphql/query/executor.rb +6 -27
- data/lib/graphql/query/literal_input.rb +89 -0
- data/lib/graphql/query/ruby_input.rb +20 -0
- data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
- data/lib/graphql/query/variables.rb +39 -0
- data/lib/graphql/scalar_type.rb +27 -5
- data/lib/graphql/schema.rb +5 -0
- data/lib/graphql/schema/type_expression.rb +28 -0
- data/lib/graphql/static_validation/literal_validator.rb +5 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +0 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- data/readme.md +3 -2
- data/spec/graphql/enum_type_spec.rb +7 -2
- data/spec/graphql/input_object_type_spec.rb +45 -0
- data/spec/graphql/language/parser_spec.rb +2 -1
- data/spec/graphql/language/transform_spec.rb +5 -0
- data/spec/graphql/query/base_execution/value_resolution_spec.rb +46 -0
- data/spec/graphql/query/context_spec.rb +37 -0
- data/spec/graphql/query/executor_spec.rb +33 -0
- data/spec/graphql/query_spec.rb +110 -26
- data/spec/graphql/schema/type_expression_spec.rb +38 -0
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +2 -2
- data/spec/support/dairy_app.rb +17 -17
- data/spec/support/dairy_data.rb +2 -2
- metadata +12 -2
@@ -1,108 +1,117 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
module GraphQL
|
2
|
+
module Language
|
3
|
+
# Extend `Parslet::Context` with a convenience method
|
4
|
+
# for building AST nodes
|
5
|
+
module ParsletContextCreateNode
|
6
|
+
refine Parslet::Context do
|
7
|
+
# @param [Symbol] name of a node constant
|
8
|
+
# @param [Hash] attributes to initialize the {Node} with
|
9
|
+
# @return [GraphQL::Language::Node] a node of type `name` with attributes `attributes`
|
10
|
+
def create_node(name, attributes)
|
11
|
+
node_class = GraphQL::Language::Nodes.const_get(name)
|
12
|
+
node_class.new(attributes)
|
13
|
+
end
|
14
|
+
end
|
14
15
|
end
|
15
16
|
|
17
|
+
using ParsletContextCreateNode
|
16
18
|
|
19
|
+
# {Transform} is a [parslet](http://kschiess.github.io/parslet/) transform for for turning the AST into objects in {GraphQL::Language::Nodes} objects.
|
20
|
+
class Transform < Parslet::Transform
|
21
|
+
def self.optional_sequence(name)
|
22
|
+
rule(name => simple(:val)) { [] }
|
23
|
+
rule(name => sequence(:val)) { val }
|
24
|
+
end
|
17
25
|
|
18
|
-
|
19
|
-
|
20
|
-
|
26
|
+
# Document
|
27
|
+
rule(document_parts: sequence(:p)) { create_node(:Document, parts: p, line: (p.first ? p.first.line : 1), col: (p.first ? p.first.col : 1))}
|
28
|
+
rule(document_parts: simple(:p)) { create_node(:Document, parts: [], line: 1, col: 1)}
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
+
# Fragment Definition
|
31
|
+
rule(
|
32
|
+
fragment_keyword: simple(:kw),
|
33
|
+
fragment_name: simple(:name),
|
34
|
+
type_condition: simple(:type),
|
35
|
+
directives: sequence(:directives),
|
36
|
+
selections: sequence(:selections)
|
37
|
+
) { create_node(:FragmentDefinition, name: name.to_s, type: type.to_s, directives: directives, selections: selections, position_source: kw)}
|
30
38
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
rule(
|
40
|
+
fragment_spread_keyword: simple(:kw),
|
41
|
+
fragment_spread_name: simple(:n),
|
42
|
+
directives: sequence(:d)
|
43
|
+
) { create_node(:FragmentSpread, name: n.to_s, directives: d, position_source: kw)}
|
36
44
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
45
|
+
rule(
|
46
|
+
fragment_spread_keyword: simple(:kw),
|
47
|
+
inline_fragment_type: simple(:n),
|
48
|
+
directives: sequence(:d),
|
49
|
+
selections: sequence(:s),
|
50
|
+
) { create_node(:InlineFragment, type: n.to_s, directives: d, selections: s, position_source: kw)}
|
43
51
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
# Operation Definition
|
53
|
+
rule(
|
54
|
+
operation_type: simple(:ot),
|
55
|
+
name: simple(:n),
|
56
|
+
variables: sequence(:v),
|
57
|
+
directives: sequence(:d),
|
58
|
+
selections: sequence(:s),
|
59
|
+
) { create_node(:OperationDefinition, operation_type: ot.to_s, name: n.to_s, variables: v, directives: d, selections: s, position_source: ot) }
|
60
|
+
optional_sequence(:optional_variables)
|
61
|
+
rule(variable_name: simple(:n), variable_type: simple(:t), variable_optional_default_value: simple(:v)) { create_node(:Variable, name: n.name, type: t, default_value: v, line: n.line, col: n.col)}
|
62
|
+
rule(variable_name: simple(:n), variable_type: simple(:t), variable_optional_default_value: sequence(:v)) { create_node(:Variable, name: n.name, type: t, default_value: v, line: n.line, col: n.col)}
|
63
|
+
rule(variable_default_value: simple(:v) ) { v }
|
64
|
+
rule(variable_default_value: sequence(:v) ) { v }
|
65
|
+
# Query short-hand
|
66
|
+
rule(unnamed_selections: sequence(:s)) { create_node(:OperationDefinition, selections: s, operation_type: "query", name: nil, variables: [], directives: [], line: s.first.line, col: s.first.col)}
|
59
67
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
+
# Field
|
69
|
+
rule(
|
70
|
+
alias: simple(:a),
|
71
|
+
field_name: simple(:name),
|
72
|
+
field_arguments: sequence(:args),
|
73
|
+
directives: sequence(:dir),
|
74
|
+
selections: sequence(:sel)
|
75
|
+
) { create_node(:Field, alias: a && a.to_s, name: name.to_s, arguments: args, directives: dir, selections: sel, position_source: [a, name].find { |part| !part.nil? }) }
|
68
76
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
77
|
+
rule(alias_name: simple(:a)) { a }
|
78
|
+
optional_sequence(:optional_field_arguments)
|
79
|
+
rule(field_argument_name: simple(:n), field_argument_value: simple(:v)) { create_node(:Argument, name: n.to_s, value: v, position_source: n)}
|
80
|
+
rule(field_argument_name: simple(:n), field_argument_value: sequence(:v)) { create_node(:Argument, name: n.to_s, value: v, position_source: n)}
|
81
|
+
optional_sequence(:optional_selections)
|
82
|
+
optional_sequence(:optional_directives)
|
75
83
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
84
|
+
# Directive
|
85
|
+
rule(directive_name: simple(:name), directive_arguments: sequence(:args)) { create_node(:Directive, name: name.to_s, arguments: args, position_source: name ) }
|
86
|
+
rule(directive_argument_name: simple(:n), directive_argument_value: simple(:v)) { create_node(:Argument, name: n.to_s, value: v, position_source: n)}
|
87
|
+
optional_sequence(:optional_directive_arguments)
|
80
88
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
89
|
+
# Type Defs
|
90
|
+
rule(type_name: simple(:n)) { create_node(:TypeName, name: n.to_s, position_source: n) }
|
91
|
+
rule(list_type: simple(:t)) { create_node(:ListType, of_type: t, line: t.line, col: t.col)}
|
92
|
+
rule(non_null_type: simple(:t)) { create_node(:NonNullType, of_type: t, line: t.line, col: t.col)}
|
85
93
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
+
# Values
|
95
|
+
rule(array: sequence(:v)) { v }
|
96
|
+
rule(boolean: simple(:v)) { v == "true" ? true : false }
|
97
|
+
rule(input_object: sequence(:v)) { create_node(:InputObject, pairs: v, line: v.first.line, col: v.first.col) }
|
98
|
+
rule(input_object_name: simple(:n), input_object_value: simple(:v)) { create_node(:Argument, name: n.to_s, value: v, position_source: n)}
|
99
|
+
rule(input_object_name: simple(:n), input_object_value: sequence(:v)) { create_node(:Argument, name: n.to_s, value: v, position_source: n)}
|
100
|
+
rule(int: simple(:v)) { v.to_i }
|
101
|
+
rule(float: simple(:v)) { v.to_f }
|
94
102
|
|
95
|
-
|
96
|
-
|
97
|
-
|
103
|
+
ESCAPES = /\\(["\\\/bfnrt])/
|
104
|
+
UTF_8 = /\\u[\da-f]{4}/i
|
105
|
+
UTF_8_REPLACE = -> (m) { [m[-4..-1].to_i(16)].pack('U') }
|
98
106
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
+
rule(string: simple(:v)) {
|
108
|
+
string = v.to_s
|
109
|
+
string.gsub!(ESCAPES, '\1')
|
110
|
+
string.gsub!(UTF_8, &UTF_8_REPLACE)
|
111
|
+
string
|
112
|
+
}
|
113
|
+
rule(variable: simple(:v)) { create_node(:VariableIdentifier, name: v.to_s, position_source: v) }
|
114
|
+
rule(enum: simple(:v)) { create_node(:Enum, name: v.to_s, position_source: v)}
|
115
|
+
end
|
107
116
|
end
|
108
117
|
end
|
@@ -1,88 +1,92 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# visitor
|
8
|
-
# #
|
9
|
-
# visitor[GraphQL::Language::Nodes::
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
module GraphQL
|
2
|
+
module Language
|
3
|
+
# Depth-first traversal through the tree, calling hooks at each stop.
|
4
|
+
#
|
5
|
+
# @example Create a visitor, add hooks, then search a document
|
6
|
+
# total_field_count = 0
|
7
|
+
# visitor = GraphQL::Language::Visitor.new
|
8
|
+
# # Whenever you find a field, increment the field count:
|
9
|
+
# visitor[GraphQL::Language::Nodes::Field] << -> (node) { total_field_count += 1 }
|
10
|
+
# # When we finish, print the field count:
|
11
|
+
# visitor[GraphQL::Language::Nodes::Document].leave << -> (node) { p total_field_count }
|
12
|
+
# visitor.visit(document)
|
13
|
+
# # => 6
|
14
|
+
#
|
15
|
+
class Visitor
|
16
|
+
# If any hook returns this value, the {Visitor} stops visiting this
|
17
|
+
# node right away
|
18
|
+
SKIP = :_skip
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
# @return [Array<Proc>] Hooks to call when entering _any_ node
|
21
|
+
attr_reader :enter
|
22
|
+
# @return [Array<Proc>] Hooks to call when leaving _any_ node
|
23
|
+
attr_reader :leave
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def initialize
|
26
|
+
@visitors = {}
|
27
|
+
@enter = []
|
28
|
+
@leave = []
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
31
|
+
# Get a {NodeVisitor} for `node_class`
|
32
|
+
# @param node_class [Class] The node class that you want to listen to
|
33
|
+
# @return [NodeVisitor]
|
34
|
+
#
|
35
|
+
# @example Run a hook whenever you enter a new Field
|
36
|
+
# visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) { p "Here's a field" }
|
37
|
+
def [](node_class)
|
38
|
+
@visitors[node_class] ||= NodeVisitor.new
|
39
|
+
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
# Visit `root` and all children, applying hooks as you go
|
42
|
+
# @param root [GraphQL::Language::Nodes::AbstractNode] some node to start parsing on
|
43
|
+
# @return [void]
|
44
|
+
def visit(root, parent=nil)
|
45
|
+
begin_visit(root, parent) &&
|
46
|
+
root.children.reduce(true) { |memo, child| memo && visit(child, root) }
|
47
|
+
end_visit(root, parent)
|
48
|
+
end
|
47
49
|
|
48
|
-
|
50
|
+
private
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
def begin_visit(node, parent)
|
53
|
+
self.class.apply_hooks(enter, node, parent)
|
54
|
+
node_visitor = self[node.class]
|
55
|
+
self.class.apply_hooks(node_visitor.enter, node, parent)
|
56
|
+
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
# Should global `leave` visitors come first or last?
|
59
|
+
def end_visit(node, parent)
|
60
|
+
self.class.apply_hooks(leave, node, parent)
|
61
|
+
node_visitor = self[node.class]
|
62
|
+
self.class.apply_hooks(node_visitor.leave, node, parent)
|
63
|
+
end
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
# If one of the visitors returns SKIP, stop visiting this node
|
66
|
+
def self.apply_hooks(hooks, node, parent)
|
67
|
+
hooks.reduce(true) { |memo, proc| memo && (proc.call(node, parent) != SKIP) }
|
68
|
+
end
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
# Collect `enter` and `leave` hooks for classes in {GraphQL::Language::Nodes}
|
71
|
+
#
|
72
|
+
# Access {NodeVisitor}s via {GraphQL::Language::Visitor#[]}
|
73
|
+
class NodeVisitor
|
74
|
+
# @return [Array<Proc>] Hooks to call when entering a node of this type
|
75
|
+
attr_reader :enter
|
76
|
+
# @return [Array<Proc>] Hooks to call when leaving a node of this type
|
77
|
+
attr_reader :leave
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
def initialize
|
80
|
+
@enter = []
|
81
|
+
@leave = []
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
# Shorthand to add a hook to the {#enter} array
|
85
|
+
# @param hook [Proc] A hook to add
|
86
|
+
def <<(hook)
|
87
|
+
enter << hook
|
88
|
+
end
|
89
|
+
end
|
86
90
|
end
|
87
91
|
end
|
88
92
|
end
|
data/lib/graphql/list_type.rb
CHANGED
@@ -13,8 +13,12 @@ class GraphQL::NonNullType < GraphQL::BaseType
|
|
13
13
|
"Non-Null"
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
of_type.
|
16
|
+
def coerce_input(value)
|
17
|
+
of_type.coerce_input(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def coerce_result(value)
|
21
|
+
of_type.coerce_result(value)
|
18
22
|
end
|
19
23
|
|
20
24
|
def kind
|
data/lib/graphql/query.rb
CHANGED
@@ -1,9 +1,23 @@
|
|
1
1
|
class GraphQL::Query
|
2
|
+
class OperationNameMissingError < StandardError
|
3
|
+
def initialize(names)
|
4
|
+
msg = "You must provide an operation name from: #{names.join(", ")}"
|
5
|
+
super(msg)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class VariableMissingError < StandardError
|
10
|
+
def initialize(name, type)
|
11
|
+
msg = "Variable #{name} of type #{type} can't be null"
|
12
|
+
super(msg)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
2
16
|
# If a resolve function returns `GraphQL::Query::DEFAULT_RESOLVE`,
|
3
17
|
# The executor will send the field's name to the target object
|
4
18
|
# and use the result.
|
5
19
|
DEFAULT_RESOLVE = :__default_resolve
|
6
|
-
attr_reader :schema, :document, :context, :fragments, :
|
20
|
+
attr_reader :schema, :document, :context, :fragments, :operations, :debug
|
7
21
|
|
8
22
|
# Prepare query `query_string` on `schema`
|
9
23
|
# @param schema [GraphQL::Schema]
|
@@ -17,13 +31,11 @@ class GraphQL::Query
|
|
17
31
|
@schema = schema
|
18
32
|
@debug = debug
|
19
33
|
@context = Context.new(values: context)
|
20
|
-
|
21
|
-
@variables = variables
|
22
34
|
@validate = validate
|
23
35
|
@operation_name = operation_name
|
24
36
|
@fragments = {}
|
25
37
|
@operations = {}
|
26
|
-
|
38
|
+
@provided_variables = variables
|
27
39
|
@document = GraphQL.parse(query_string)
|
28
40
|
@document.parts.each do |part|
|
29
41
|
if part.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
@@ -40,20 +52,57 @@ class GraphQL::Query
|
|
40
52
|
return { "errors" => validation_errors }
|
41
53
|
end
|
42
54
|
|
43
|
-
@result ||= Executor.new(self
|
55
|
+
@result ||= Executor.new(self).result
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# This is the operation to run for this query.
|
60
|
+
# If more than one operation is present, it must be named at runtime.
|
61
|
+
# @return [GraphQL::Language::Nodes::OperationDefinition, nil]
|
62
|
+
def selected_operation
|
63
|
+
@selected_operation ||= find_operation(@operations, @operation_name)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Determine the values for variables of this query, using default values
|
67
|
+
# if a value isn't provided at runtime.
|
68
|
+
#
|
69
|
+
# Raises if a non-null variable isn't provided at runtime.
|
70
|
+
# @return [GraphQL::Query::Variables] Variables to apply to this query
|
71
|
+
def variables
|
72
|
+
@variables ||= GraphQL::Query::Variables.new(
|
73
|
+
schema,
|
74
|
+
selected_operation.variables,
|
75
|
+
@provided_variables
|
76
|
+
)
|
44
77
|
end
|
45
78
|
|
46
79
|
private
|
47
80
|
|
48
81
|
def validation_errors
|
49
|
-
@validation_errors ||=
|
82
|
+
@validation_errors ||= schema.static_validator.validate(document)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def find_operation(operations, operation_name)
|
87
|
+
if operations.length == 1
|
88
|
+
operations.values.first
|
89
|
+
elsif operations.length == 0
|
90
|
+
nil
|
91
|
+
elsif !operations.key?(operation_name)
|
92
|
+
raise OperationNameMissingError, operations.keys
|
93
|
+
else
|
94
|
+
operations[operation_name]
|
95
|
+
end
|
50
96
|
end
|
51
97
|
end
|
52
98
|
|
53
99
|
require 'graphql/query/arguments'
|
54
100
|
require 'graphql/query/base_execution'
|
55
|
-
require 'graphql/query/
|
56
|
-
require 'graphql/query/type_resolver'
|
101
|
+
require 'graphql/query/context'
|
57
102
|
require 'graphql/query/directive_chain'
|
58
103
|
require 'graphql/query/executor'
|
59
|
-
require 'graphql/query/
|
104
|
+
require 'graphql/query/literal_input'
|
105
|
+
require 'graphql/query/ruby_input'
|
106
|
+
require 'graphql/query/serial_execution'
|
107
|
+
require 'graphql/query/type_resolver'
|
108
|
+
require 'graphql/query/variables'
|