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.
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