graphql 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|