graphql 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +46 -29
- data/lib/graphql/call.rb +1 -1
- data/lib/graphql/connection.rb +12 -35
- data/lib/graphql/field.rb +7 -177
- data/lib/graphql/field_definer.rb +5 -15
- data/lib/graphql/introspection/{call_node.rb → call_type.rb} +1 -1
- data/lib/graphql/introspection/field_type.rb +10 -0
- data/lib/graphql/introspection/{root_call_node.rb → root_call_type.rb} +6 -2
- data/lib/graphql/introspection/schema_type.rb +17 -0
- data/lib/graphql/introspection/{type_node.rb → type_type.rb} +4 -2
- data/lib/graphql/node.rb +118 -42
- data/lib/graphql/{parser.rb → parser/parser.rb} +9 -4
- data/lib/graphql/{transform.rb → parser/transform.rb} +4 -2
- data/lib/graphql/query.rb +33 -10
- data/lib/graphql/root_call.rb +26 -13
- data/lib/graphql/root_call_argument.rb +3 -1
- data/lib/graphql/root_call_argument_definer.rb +3 -7
- data/lib/graphql/schema/all.rb +46 -0
- data/lib/graphql/{schema.rb → schema/schema.rb} +27 -39
- data/lib/graphql/schema/schema_validation.rb +32 -0
- data/lib/graphql/syntax/fragment.rb +7 -0
- data/lib/graphql/syntax/query.rb +3 -2
- data/lib/graphql/types/boolean_type.rb +3 -0
- data/lib/graphql/types/number_type.rb +3 -0
- data/lib/graphql/types/object_type.rb +6 -0
- data/lib/graphql/types/string_type.rb +3 -0
- data/lib/graphql/version.rb +1 -1
- data/readme.md +46 -5
- data/spec/graphql/node_spec.rb +6 -5
- data/spec/graphql/{parser_spec.rb → parser/parser_spec.rb} +31 -2
- data/spec/graphql/{transform_spec.rb → parser/transform_spec.rb} +16 -2
- data/spec/graphql/query_spec.rb +27 -9
- data/spec/graphql/root_call_spec.rb +15 -1
- data/spec/graphql/{schema_spec.rb → schema/schema_spec.rb} +15 -50
- data/spec/graphql/schema/schema_validation_spec.rb +48 -0
- data/spec/support/nodes.rb +31 -28
- metadata +47 -47
- data/lib/graphql/introspection/field_node.rb +0 -19
- data/lib/graphql/introspection/schema_node.rb +0 -17
- data/lib/graphql/types/boolean_field.rb +0 -3
- data/lib/graphql/types/connection_field.rb +0 -30
- data/lib/graphql/types/cursor_field.rb +0 -9
- data/lib/graphql/types/number_field.rb +0 -3
- data/lib/graphql/types/object_field.rb +0 -8
- data/lib/graphql/types/string_field.rb +0 -3
- data/lib/graphql/types/type_field.rb +0 -6
- data/spec/graphql/field_spec.rb +0 -63
@@ -1,8 +1,8 @@
|
|
1
|
-
class GraphQL::Introspection::
|
1
|
+
class GraphQL::Introspection::RootCallType < GraphQL::Node
|
2
2
|
exposes "GraphQL::RootCall"
|
3
3
|
field.string(:name)
|
4
4
|
field.string(:returns)
|
5
|
-
field.
|
5
|
+
field.introspection_connection(:arguments)
|
6
6
|
|
7
7
|
|
8
8
|
def returns
|
@@ -13,4 +13,8 @@ class GraphQL::Introspection::RootCallNode < GraphQL::Node
|
|
13
13
|
def name
|
14
14
|
schema_name
|
15
15
|
end
|
16
|
+
|
17
|
+
def arguments
|
18
|
+
target.arguments.values
|
19
|
+
end
|
16
20
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class GraphQL::Introspection::SchemaType < GraphQL::Node
|
2
|
+
exposes "GraphQL::Schema::Schema"
|
3
|
+
field.introspection_connection(:calls)
|
4
|
+
field.introspection_connection(:types)
|
5
|
+
|
6
|
+
def cursor
|
7
|
+
"schema"
|
8
|
+
end
|
9
|
+
|
10
|
+
def types
|
11
|
+
@target.types.values
|
12
|
+
end
|
13
|
+
|
14
|
+
def calls
|
15
|
+
target.calls.values
|
16
|
+
end
|
17
|
+
end
|
@@ -1,11 +1,13 @@
|
|
1
|
-
class GraphQL::Introspection::
|
1
|
+
class GraphQL::Introspection::TypeType < GraphQL::Node
|
2
2
|
exposes "GraphQL::Node"
|
3
|
+
type "__type__"
|
3
4
|
field.string(:name)
|
4
5
|
field.string(:description)
|
5
|
-
field.
|
6
|
+
field.introspection_connection(:fields)
|
6
7
|
|
7
8
|
cursor :name
|
8
9
|
|
10
|
+
# they're actually {FieldMapping}s
|
9
11
|
def fields
|
10
12
|
target.all_fields.values
|
11
13
|
end
|
data/lib/graphql/node.rb
CHANGED
@@ -5,8 +5,9 @@
|
|
5
5
|
# - Extend `GraphQL::Node`
|
6
6
|
# - Declare what this node will wrap with {.exposes}
|
7
7
|
# - Declare fields with {.field}
|
8
|
+
# - Declare calls with {.call}
|
8
9
|
#
|
9
|
-
# @example
|
10
|
+
# @example Expose a class in your app
|
10
11
|
# class PostNode < GraphQL::Node
|
11
12
|
# exposes('Post')
|
12
13
|
#
|
@@ -18,19 +19,35 @@
|
|
18
19
|
# field.connection(:comments)
|
19
20
|
# end
|
20
21
|
#
|
21
|
-
|
22
|
+
# @example Expose a data type
|
23
|
+
# class DateType < GraphQL::Node
|
24
|
+
# exposes "Date"
|
25
|
+
# type :date
|
26
|
+
# call :minus_days, -> (prev_value, minus_days) { prev_value - minus_days.to_i }
|
27
|
+
# field.number(:year)
|
28
|
+
# field.number(:month)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # now you could use it
|
32
|
+
# class PostNode
|
33
|
+
# field.date(:published_at)
|
34
|
+
# end
|
22
35
|
class GraphQL::Node
|
23
|
-
# The object wrapped by this `Node
|
36
|
+
# The object wrapped by this `Node`, _before_ calls are applied
|
37
|
+
attr_reader :original_target
|
38
|
+
# The object wrapped by this `Node`, _after_ calls are applied
|
24
39
|
attr_reader :target
|
25
40
|
# Fields parsed from the query string
|
26
41
|
attr_reader :syntax_fields
|
27
42
|
# The query to which this `Node` belongs. Used for accessing its {Query#context}.
|
28
43
|
attr_reader :query
|
29
44
|
|
30
|
-
def initialize(target=nil, fields:, query:)
|
31
|
-
@target = target
|
45
|
+
def initialize(target=nil, fields:, query:, calls: [])
|
32
46
|
@query = query
|
47
|
+
@calls = calls
|
33
48
|
@syntax_fields = fields
|
49
|
+
@original_target = target
|
50
|
+
@target = apply_calls(target)
|
34
51
|
end
|
35
52
|
|
36
53
|
# If the target responds to `method_name`, send it to target.
|
@@ -44,20 +61,29 @@ class GraphQL::Node
|
|
44
61
|
|
45
62
|
# Looks up {#syntax_fields} against this node and returns the results
|
46
63
|
def as_result
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
64
|
+
@as_result ||= begin
|
65
|
+
json = {}
|
66
|
+
syntax_fields.each do |syntax_field|
|
67
|
+
key_name = syntax_field.alias_name || syntax_field.identifier
|
68
|
+
if key_name == 'node'
|
69
|
+
clone_node = self.class.new(target, fields: syntax_field.fields, query: query, calls: syntax_field.calls)
|
70
|
+
json[key_name] = clone_node.as_result
|
71
|
+
elsif key_name == 'cursor'
|
72
|
+
json[key_name] = cursor
|
73
|
+
elsif key_name[0] == "$"
|
74
|
+
fragment = query.fragments[key_name]
|
75
|
+
# execute the fragment and merge it into this result
|
76
|
+
clone_node = self.class.new(target, fields: fragment.fields, query: query, calls: @calls)
|
77
|
+
json.merge!(clone_node.as_result)
|
78
|
+
else
|
79
|
+
field = get_field(syntax_field)
|
80
|
+
new_target = public_send(field.name)
|
81
|
+
new_node = field.type_class.new(new_target, fields: syntax_field.fields, query: query, calls: syntax_field.calls)
|
82
|
+
json[key_name] = new_node.as_result
|
83
|
+
end
|
58
84
|
end
|
85
|
+
json
|
59
86
|
end
|
60
|
-
json
|
61
87
|
end
|
62
88
|
|
63
89
|
# The object passed to {Query#initialize} as `context`.
|
@@ -65,25 +91,38 @@ class GraphQL::Node
|
|
65
91
|
query.context
|
66
92
|
end
|
67
93
|
|
94
|
+
def __type__
|
95
|
+
self.class
|
96
|
+
end
|
68
97
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
98
|
+
def apply_calls(value)
|
99
|
+
finished_value(value)
|
100
|
+
end
|
101
|
+
|
102
|
+
def finished_value(raw_value)
|
103
|
+
@finished_value ||= begin
|
104
|
+
val = raw_value
|
105
|
+
@calls.each do |call|
|
106
|
+
registered_call = self.class.calls[call.identifier]
|
107
|
+
if registered_call.nil?
|
108
|
+
raise "Call not found: #{self.class.name}##{call.identifier}"
|
109
|
+
end
|
110
|
+
val = registered_call.lambda.call(val, *call.arguments)
|
75
111
|
end
|
112
|
+
val
|
76
113
|
end
|
114
|
+
end
|
77
115
|
|
116
|
+
class << self
|
78
117
|
# @param [String] class_name name of the class this node will wrap.
|
79
|
-
def exposes(
|
80
|
-
@
|
118
|
+
def exposes(*exposes_class_names)
|
119
|
+
@exposes_class_names = exposes_class_names
|
81
120
|
GraphQL::SCHEMA.add_type(self)
|
82
121
|
end
|
83
122
|
|
84
|
-
# The
|
85
|
-
def
|
86
|
-
@
|
123
|
+
# The names of the classes wrapped by this node
|
124
|
+
def exposes_class_names
|
125
|
+
@exposes_class_names || []
|
87
126
|
end
|
88
127
|
|
89
128
|
# @param [String] describe
|
@@ -110,17 +149,15 @@ class GraphQL::Node
|
|
110
149
|
end
|
111
150
|
|
112
151
|
def default_schema_name
|
113
|
-
name.split("::").last.sub(/Node$/, '').underscore
|
152
|
+
name.split("::").last.sub(/(Node|Type)$/, '').underscore
|
114
153
|
end
|
115
154
|
|
116
155
|
# @param [String] field_name name of the field to be used as the cursor
|
117
156
|
# Declares what field will be used as the cursor for this node.
|
118
157
|
def cursor(field_name)
|
119
158
|
define_method "cursor" do
|
120
|
-
|
121
|
-
|
122
|
-
cursor = GraphQL::Types::CursorField.new(field.as_result)
|
123
|
-
cursor.as_result
|
159
|
+
field_mapping = self.class.all_fields[field_name.to_s]
|
160
|
+
public_send(field_mapping.name).to_s
|
124
161
|
end
|
125
162
|
end
|
126
163
|
|
@@ -146,23 +183,62 @@ class GraphQL::Node
|
|
146
183
|
def remove_field(field_name)
|
147
184
|
own_fields.delete(field_name.to_s)
|
148
185
|
end
|
186
|
+
|
187
|
+
# Can the node handle a field with this name?
|
188
|
+
def respond_to_field?(field_name)
|
189
|
+
if all_fields[field_name.to_s].blank?
|
190
|
+
false
|
191
|
+
elsif method_defined?(field_name)
|
192
|
+
true
|
193
|
+
elsif exposes_class_names.any? do |exposes_class_name|
|
194
|
+
exposes_class = Object.const_get(exposes_class_name)
|
195
|
+
exposes_class.method_defined?(field_name) || exposes_class.respond_to?(field_name)
|
196
|
+
end
|
197
|
+
true
|
198
|
+
else
|
199
|
+
false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def calls
|
204
|
+
superclass.calls.merge(own_calls)
|
205
|
+
rescue NoMethodError
|
206
|
+
{}
|
207
|
+
end
|
208
|
+
# @param [String] name the identifier for this call
|
209
|
+
# @param [lambda] operation the transformation this call makes
|
210
|
+
#
|
211
|
+
# Define a call that can be made on nodes of this type.
|
212
|
+
# The `lambda` receives arguments:
|
213
|
+
# - 1: `previous_value` -- the value of this node
|
214
|
+
# - *: arguments passed in the query (as strings)
|
215
|
+
#
|
216
|
+
# @example
|
217
|
+
# # upcase a string field:
|
218
|
+
# call :upcase, -> (prev_value) { prev_value.upcase }
|
219
|
+
# @example
|
220
|
+
# # tests a number field:
|
221
|
+
# call :greater_than, -> (prev_value, test_value) { prev_value > test_value.to_f }
|
222
|
+
# # (`test_value` is passed in as a string)
|
223
|
+
def call(name, lambda)
|
224
|
+
own_calls[name.to_s] = GraphQL::Call.new(name: name.to_s, lambda: lambda)
|
225
|
+
end
|
226
|
+
|
227
|
+
def own_calls
|
228
|
+
@own_calls ||= {}
|
229
|
+
end
|
149
230
|
end
|
150
231
|
|
151
232
|
private
|
152
233
|
|
153
234
|
def get_field(syntax_field)
|
154
|
-
|
235
|
+
field_mapping = self.class.all_fields[syntax_field.identifier]
|
155
236
|
if syntax_field.identifier == "cursor"
|
156
237
|
cursor
|
157
|
-
elsif
|
158
|
-
raise GraphQL::FieldNotDefinedError.new(self.class
|
238
|
+
elsif field_mapping.nil?
|
239
|
+
raise GraphQL::FieldNotDefinedError.new(self.class, syntax_field.identifier)
|
159
240
|
else
|
160
|
-
|
161
|
-
query: query,
|
162
|
-
owner: self,
|
163
|
-
calls: syntax_field.calls,
|
164
|
-
fields: syntax_field.fields,
|
165
|
-
)
|
241
|
+
field_mapping
|
166
242
|
end
|
167
243
|
end
|
168
244
|
end
|
@@ -1,22 +1,27 @@
|
|
1
1
|
# Parser is a [parslet](http://kschiess.github.io/parslet/) parser for parsing queries.
|
2
2
|
#
|
3
3
|
# If it failes to parse, a {SyntaxError} is raised.
|
4
|
-
class GraphQL::Parser < Parslet::Parser
|
4
|
+
class GraphQL::Parser::Parser < Parslet::Parser
|
5
5
|
root(:query)
|
6
|
-
rule(:query) { node.repeat.as(:nodes) >> variable.repeat.as(:variables) }
|
6
|
+
rule(:query) { node.repeat.as(:nodes) >> variable.repeat.as(:variables) >> fragment.repeat.as(:fragments) }
|
7
|
+
|
7
8
|
# node
|
8
9
|
rule(:node) { space? >> call >> space? >> fields.as(:fields) }
|
9
10
|
|
11
|
+
# fragment
|
12
|
+
rule(:fragment) { space? >> fragment_identifier >> str(":") >> space? >> fields.as(:fields) >> space?}
|
13
|
+
rule(:fragment_identifier) { (str("$") >> name).as(:identifier) }
|
14
|
+
|
10
15
|
# field set
|
11
16
|
rule(:fields) { str("{") >> space? >> (field >> separator?).repeat(1) >> space? >> str("}") >> space?}
|
12
17
|
|
13
|
-
#call
|
18
|
+
# call
|
14
19
|
rule(:call) { identifier >> str("(") >> (argument.as(:argument) >> separator?).repeat(0).as(:arguments) >> str(")") }
|
15
20
|
rule(:dot) { str(".") }
|
16
21
|
rule(:argument) { (identifier | variable_identifier)}
|
17
22
|
|
18
23
|
# field
|
19
|
-
rule(:field) { identifier >> call_chain.maybe >> alias_name.maybe >> space? >> fields.as(:fields).maybe }
|
24
|
+
rule(:field) { (identifier | fragment_identifier) >> call_chain.maybe >> alias_name.maybe >> space? >> fields.as(:fields).maybe }
|
20
25
|
rule(:call_chain) { (dot >> call).repeat(0).as(:calls) }
|
21
26
|
rule(:alias_name) { space >> str("as") >> space >> name.as(:alias_name) }
|
22
27
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# {Transform} is a [parslet](http://kschiess.github.io/parslet/) transform for for turning the AST into objects in {GraphQL::Syntax}.
|
2
|
-
class GraphQL::Transform < Parslet::Transform
|
2
|
+
class GraphQL::Parser::Transform < Parslet::Transform
|
3
3
|
# query
|
4
|
-
rule(nodes: sequence(:n), variables: sequence(:v)) { GraphQL::Syntax::Query.new(nodes: n, variables: v)}
|
4
|
+
rule(nodes: sequence(:n), variables: sequence(:v), fragments: sequence(:f)) { GraphQL::Syntax::Query.new(nodes: n, variables: v, fragments: f)}
|
5
5
|
# node
|
6
6
|
rule(identifier: simple(:i), arguments: sequence(:a), fields: sequence(:f)) {GraphQL::Syntax::Node.new(identifier: i.to_s, arguments: a, fields: f)}
|
7
7
|
# field
|
@@ -17,4 +17,6 @@ class GraphQL::Transform < Parslet::Transform
|
|
17
17
|
rule(identifier: simple(:i)) { i.to_s }
|
18
18
|
# variable
|
19
19
|
rule(identifier: simple(:i), json_string: simple(:j)) { GraphQL::Syntax::Variable.new(identifier: i.to_s, json_string: j.to_s)}
|
20
|
+
# fragment
|
21
|
+
rule(identifier: simple(:i), fields: sequence(:f)) { GraphQL::Syntax::Fragment.new(identifier: i, fields: f)}
|
20
22
|
end
|
data/lib/graphql/query.rb
CHANGED
@@ -28,22 +28,25 @@ class GraphQL::Query
|
|
28
28
|
@as_result ||= execute!
|
29
29
|
end
|
30
30
|
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
# Provides access to query variables, raises an error if not found
|
32
|
+
# @example
|
33
|
+
# query.variables["<person_data>"]
|
34
|
+
def variables
|
35
|
+
@variables ||= LookupHash.new("variable", @root.variables)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provides access to query fragments, raises an error if not found
|
39
|
+
# @example
|
40
|
+
# query.fragments["$personData"]
|
41
|
+
def fragments
|
42
|
+
@fragments ||= LookupHash.new("fragment", @root.fragments)
|
39
43
|
end
|
40
44
|
|
41
45
|
private
|
42
46
|
|
43
47
|
def execute!
|
44
48
|
root_syntax_node = root.nodes[0]
|
45
|
-
|
46
|
-
root_call_class = GraphQL::SCHEMA.get_call(root_call_identifier)
|
49
|
+
root_call_class = GraphQL::SCHEMA.get_call(root_syntax_node.identifier)
|
47
50
|
root_call = root_call_class.new(query: self, syntax_arguments: root_syntax_node.arguments)
|
48
51
|
result_object = root_call.as_result
|
49
52
|
return_declarations = root_call_class.return_declarations
|
@@ -83,4 +86,24 @@ class GraphQL::Query
|
|
83
86
|
line, col = error.cause.source.line_and_column
|
84
87
|
raise GraphQL::SyntaxError.new(line, col, query_string)
|
85
88
|
end
|
89
|
+
|
90
|
+
# Caches items by name, raises an error if not found
|
91
|
+
class LookupHash
|
92
|
+
attr_reader :item_name, :items
|
93
|
+
def initialize(item_name, items)
|
94
|
+
@items = items
|
95
|
+
@item_name = item_name
|
96
|
+
@storage = Hash.new do |hash, identifier|
|
97
|
+
value = items.find {|i| i.identifier == identifier }
|
98
|
+
if value.blank?
|
99
|
+
"No #{item_name} found for #{identifier}, defined #{item_name}s are: #{items.map(&:identifier)}"
|
100
|
+
end
|
101
|
+
hash[identifier] = value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def [](identifier)
|
106
|
+
@storage[identifier]
|
107
|
+
end
|
108
|
+
end
|
86
109
|
end
|
data/lib/graphql/root_call.rb
CHANGED
@@ -43,7 +43,7 @@ class GraphQL::RootCall
|
|
43
43
|
@arguments = syntax_arguments.each_with_index.map do |syntax_arg, idx|
|
44
44
|
|
45
45
|
value = if syntax_arg[0] == "<"
|
46
|
-
query.
|
46
|
+
query.variables[syntax_arg].json_string
|
47
47
|
else
|
48
48
|
syntax_arg
|
49
49
|
end
|
@@ -139,14 +139,34 @@ class GraphQL::RootCall
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def own_arguments
|
142
|
-
@
|
142
|
+
@own_arguments ||= {}
|
143
143
|
end
|
144
144
|
|
145
145
|
def arguments
|
146
|
-
|
147
|
-
own + superclass.arguments
|
146
|
+
superclass.arguments.merge(own_arguments)
|
148
147
|
rescue NoMethodError
|
149
|
-
|
148
|
+
{}
|
149
|
+
end
|
150
|
+
|
151
|
+
def add_argument(argument)
|
152
|
+
existing_argument = arguments[argument.name]
|
153
|
+
if existing_argument.blank?
|
154
|
+
# only assign an index if this variable wasn't already defined
|
155
|
+
argument.index = arguments.keys.length
|
156
|
+
else
|
157
|
+
# use the same index as the already-defined one
|
158
|
+
argument.index = existing_argument.index
|
159
|
+
end
|
160
|
+
|
161
|
+
own_arguments[argument.name] = argument
|
162
|
+
end
|
163
|
+
|
164
|
+
def argument_at_index(idx)
|
165
|
+
if arguments.values.first.any_number
|
166
|
+
arguments.values.first
|
167
|
+
else
|
168
|
+
arguments.values.find { |arg| arg.index == idx } || raise("No argument found for #{name} at index #{JSON.dump(idx)} (argument indexes: #{arguments.values.map(&:index)})")
|
169
|
+
end
|
150
170
|
end
|
151
171
|
end
|
152
172
|
|
@@ -158,16 +178,9 @@ class GraphQL::RootCall
|
|
158
178
|
"string" => String,
|
159
179
|
}
|
160
180
|
|
161
|
-
def argument_for_index(idx)
|
162
|
-
if self.class.arguments.first.any_number
|
163
|
-
self.class.arguments.first
|
164
|
-
else
|
165
|
-
self.class.arguments[idx]
|
166
|
-
end
|
167
|
-
end
|
168
181
|
|
169
182
|
def typecast(idx, value)
|
170
|
-
arg_dec =
|
183
|
+
arg_dec = self.class.argument_at_index(idx)
|
171
184
|
expected_type = arg_dec.type
|
172
185
|
expected_type_class = TYPE_CHECKS[expected_type]
|
173
186
|
|