graphql 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +46 -29
  3. data/lib/graphql/call.rb +1 -1
  4. data/lib/graphql/connection.rb +12 -35
  5. data/lib/graphql/field.rb +7 -177
  6. data/lib/graphql/field_definer.rb +5 -15
  7. data/lib/graphql/introspection/{call_node.rb → call_type.rb} +1 -1
  8. data/lib/graphql/introspection/field_type.rb +10 -0
  9. data/lib/graphql/introspection/{root_call_node.rb → root_call_type.rb} +6 -2
  10. data/lib/graphql/introspection/schema_type.rb +17 -0
  11. data/lib/graphql/introspection/{type_node.rb → type_type.rb} +4 -2
  12. data/lib/graphql/node.rb +118 -42
  13. data/lib/graphql/{parser.rb → parser/parser.rb} +9 -4
  14. data/lib/graphql/{transform.rb → parser/transform.rb} +4 -2
  15. data/lib/graphql/query.rb +33 -10
  16. data/lib/graphql/root_call.rb +26 -13
  17. data/lib/graphql/root_call_argument.rb +3 -1
  18. data/lib/graphql/root_call_argument_definer.rb +3 -7
  19. data/lib/graphql/schema/all.rb +46 -0
  20. data/lib/graphql/{schema.rb → schema/schema.rb} +27 -39
  21. data/lib/graphql/schema/schema_validation.rb +32 -0
  22. data/lib/graphql/syntax/fragment.rb +7 -0
  23. data/lib/graphql/syntax/query.rb +3 -2
  24. data/lib/graphql/types/boolean_type.rb +3 -0
  25. data/lib/graphql/types/number_type.rb +3 -0
  26. data/lib/graphql/types/object_type.rb +6 -0
  27. data/lib/graphql/types/string_type.rb +3 -0
  28. data/lib/graphql/version.rb +1 -1
  29. data/readme.md +46 -5
  30. data/spec/graphql/node_spec.rb +6 -5
  31. data/spec/graphql/{parser_spec.rb → parser/parser_spec.rb} +31 -2
  32. data/spec/graphql/{transform_spec.rb → parser/transform_spec.rb} +16 -2
  33. data/spec/graphql/query_spec.rb +27 -9
  34. data/spec/graphql/root_call_spec.rb +15 -1
  35. data/spec/graphql/{schema_spec.rb → schema/schema_spec.rb} +15 -50
  36. data/spec/graphql/schema/schema_validation_spec.rb +48 -0
  37. data/spec/support/nodes.rb +31 -28
  38. metadata +47 -47
  39. data/lib/graphql/introspection/field_node.rb +0 -19
  40. data/lib/graphql/introspection/schema_node.rb +0 -17
  41. data/lib/graphql/types/boolean_field.rb +0 -3
  42. data/lib/graphql/types/connection_field.rb +0 -30
  43. data/lib/graphql/types/cursor_field.rb +0 -9
  44. data/lib/graphql/types/number_field.rb +0 -3
  45. data/lib/graphql/types/object_field.rb +0 -8
  46. data/lib/graphql/types/string_field.rb +0 -3
  47. data/lib/graphql/types/type_field.rb +0 -6
  48. data/spec/graphql/field_spec.rb +0 -63
@@ -1,4 +1,4 @@
1
- class GraphQL::Introspection::CallNode < GraphQL::Node
1
+ class GraphQL::Introspection::CallType < GraphQL::Node
2
2
  exposes "GraphQL::Call"
3
3
  field.string(:name)
4
4
  field.string(:arguments)
@@ -0,0 +1,10 @@
1
+ class GraphQL::Introspection::FieldType < GraphQL::Node
2
+ exposes "GraphQL::Field"
3
+ field.string(:name)
4
+ field.string(:type)
5
+ field.introspection_connection(:calls)
6
+
7
+ def calls
8
+ target.type_class.calls.values
9
+ end
10
+ end
@@ -1,8 +1,8 @@
1
- class GraphQL::Introspection::RootCallNode < GraphQL::Node
1
+ class GraphQL::Introspection::RootCallType < GraphQL::Node
2
2
  exposes "GraphQL::RootCall"
3
3
  field.string(:name)
4
4
  field.string(:returns)
5
- field.connection(:arguments)
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::TypeNode < GraphQL::Node
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.connection(:fields)
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
- json = {}
48
- syntax_fields.each do |syntax_field|
49
- key_name = syntax_field.alias_name || syntax_field.identifier
50
- if key_name == 'node'
51
- clone_node = self.class.new(target, fields: syntax_field.fields, query: query)
52
- json[key_name] = clone_node.as_result
53
- elsif key_name == 'cursor'
54
- json[key_name] = cursor
55
- else
56
- field = get_field(syntax_field)
57
- json[key_name] = field.as_result
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
- class << self
70
- # Registers this node in {GraphQL::SCHEMA}
71
- def inherited(child_class)
72
- # use name to prevent autoloading Connection
73
- if child_class.ancestors.map(&:name).include?("GraphQL::Connection")
74
- GraphQL::SCHEMA.add_connection(child_class)
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(ruby_class_name)
80
- @ruby_class_name = ruby_class_name
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 name of the class wrapped by this node
85
- def ruby_class_name
86
- @ruby_class_name
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
- field_class = self.class.all_fields[field_name.to_s]
121
- field = field_class.new(query: query, owner: self, calls: [])
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
- field_class = self.class.all_fields[syntax_field.identifier]
235
+ field_mapping = self.class.all_fields[syntax_field.identifier]
155
236
  if syntax_field.identifier == "cursor"
156
237
  cursor
157
- elsif field_class.nil?
158
- raise GraphQL::FieldNotDefinedError.new(self.class.name, syntax_field.identifier)
238
+ elsif field_mapping.nil?
239
+ raise GraphQL::FieldNotDefinedError.new(self.class, syntax_field.identifier)
159
240
  else
160
- field_class.new(
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
- # @param [String] identifier
32
- # returns a query variable named `identifier`, otherwise raises.
33
- def get_variable(identifier)
34
- syntax_var = @root.variables.find { |v| v.identifier == identifier }
35
- if syntax_var.blank?
36
- raise "No variable found for #{identifier}, defined variables are #{@root.variables.map(&:identifier)}"
37
- end
38
- syntax_var
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
- root_call_identifier = root_syntax_node.identifier
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
@@ -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.get_variable(syntax_arg).json_string
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
- @argument && @argument.arguments
142
+ @own_arguments ||= {}
143
143
  end
144
144
 
145
145
  def arguments
146
- own = own_arguments || []
147
- own + superclass.arguments
146
+ superclass.arguments.merge(own_arguments)
148
147
  rescue NoMethodError
149
- own
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 = argument_for_index(idx)
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