graphql 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graph_ql/directive.rb +36 -0
  3. data/lib/graph_ql/directives/directive_chain.rb +33 -0
  4. data/lib/graph_ql/directives/include_directive.rb +15 -0
  5. data/lib/graph_ql/directives/skip_directive.rb +15 -0
  6. data/lib/graph_ql/enum.rb +34 -0
  7. data/lib/graph_ql/fields/abstract_field.rb +37 -0
  8. data/lib/graph_ql/fields/access_field.rb +24 -0
  9. data/lib/graph_ql/fields/field.rb +34 -0
  10. data/lib/graph_ql/interface.rb +14 -0
  11. data/lib/graph_ql/introspection/arguments_field.rb +5 -0
  12. data/lib/graph_ql/introspection/directive_type.rb +12 -0
  13. data/lib/graph_ql/introspection/enum_value_type.rb +10 -0
  14. data/lib/graph_ql/introspection/enum_values_field.rb +15 -0
  15. data/lib/graph_ql/introspection/field_type.rb +11 -0
  16. data/lib/graph_ql/introspection/fields_field.rb +14 -0
  17. data/lib/graph_ql/introspection/input_fields_field.rb +12 -0
  18. data/lib/graph_ql/introspection/input_value_type.rb +10 -0
  19. data/lib/graph_ql/introspection/of_type_field.rb +12 -0
  20. data/lib/graph_ql/introspection/possible_types_field.rb +12 -0
  21. data/lib/graph_ql/introspection/schema_type.rb +32 -0
  22. data/lib/graph_ql/introspection/type_kind_enum.rb +7 -0
  23. data/lib/graph_ql/introspection/type_type.rb +22 -0
  24. data/lib/graph_ql/parser/nodes.rb +72 -0
  25. data/lib/graph_ql/parser/parser.rb +108 -0
  26. data/lib/graph_ql/parser/transform.rb +86 -0
  27. data/lib/graph_ql/parser/visitor.rb +47 -0
  28. data/lib/graph_ql/query.rb +50 -0
  29. data/lib/graph_ql/query/arguments.rb +25 -0
  30. data/lib/graph_ql/query/field_resolution_strategy.rb +83 -0
  31. data/lib/graph_ql/query/fragment_spread_resolution_strategy.rb +16 -0
  32. data/lib/graph_ql/query/inline_fragment_resolution_strategy.rb +14 -0
  33. data/lib/graph_ql/query/operation_resolver.rb +28 -0
  34. data/lib/graph_ql/query/selection_resolver.rb +20 -0
  35. data/lib/graph_ql/query/type_resolver.rb +19 -0
  36. data/lib/graph_ql/repl.rb +27 -0
  37. data/lib/graph_ql/schema.rb +30 -0
  38. data/lib/graph_ql/schema/type_reducer.rb +44 -0
  39. data/lib/graph_ql/type_kinds.rb +15 -0
  40. data/lib/graph_ql/types/abstract_type.rb +14 -0
  41. data/lib/graph_ql/types/boolean_type.rb +6 -0
  42. data/lib/graph_ql/types/float_type.rb +6 -0
  43. data/lib/graph_ql/types/input_object_type.rb +17 -0
  44. data/lib/graph_ql/types/input_value.rb +10 -0
  45. data/lib/graph_ql/types/int_type.rb +6 -0
  46. data/lib/graph_ql/types/list_type.rb +10 -0
  47. data/lib/graph_ql/types/non_null_type.rb +18 -0
  48. data/lib/graph_ql/types/non_null_with_bang.rb +5 -0
  49. data/lib/graph_ql/types/object_type.rb +62 -0
  50. data/lib/graph_ql/types/scalar_type.rb +5 -0
  51. data/lib/graph_ql/types/string_type.rb +6 -0
  52. data/lib/graph_ql/types/type_definer.rb +16 -0
  53. data/lib/graph_ql/union.rb +35 -0
  54. data/lib/graph_ql/validations/fields_are_defined_on_type.rb +44 -0
  55. data/lib/graph_ql/validations/fields_will_merge.rb +80 -0
  56. data/lib/graph_ql/validations/fragments_are_used.rb +24 -0
  57. data/lib/graph_ql/validator.rb +29 -0
  58. data/lib/graph_ql/version.rb +3 -0
  59. data/lib/graphql.rb +92 -99
  60. data/readme.md +17 -177
  61. data/spec/graph_ql/directive_spec.rb +81 -0
  62. data/spec/graph_ql/enum_spec.rb +5 -0
  63. data/spec/graph_ql/fields/field_spec.rb +10 -0
  64. data/spec/graph_ql/interface_spec.rb +13 -0
  65. data/spec/graph_ql/introspection/directive_type_spec.rb +40 -0
  66. data/spec/graph_ql/introspection/schema_type_spec.rb +39 -0
  67. data/spec/graph_ql/introspection/type_type_spec.rb +104 -0
  68. data/spec/graph_ql/parser/parser_spec.rb +120 -0
  69. data/spec/graph_ql/parser/transform_spec.rb +109 -0
  70. data/spec/graph_ql/parser/visitor_spec.rb +31 -0
  71. data/spec/graph_ql/query/operation_resolver_spec.rb +14 -0
  72. data/spec/graph_ql/query_spec.rb +82 -0
  73. data/spec/graph_ql/schema/type_reducer_spec.rb +24 -0
  74. data/spec/graph_ql/types/input_object_type_spec.rb +12 -0
  75. data/spec/graph_ql/types/object_type_spec.rb +35 -0
  76. data/spec/graph_ql/union_spec.rb +27 -0
  77. data/spec/graph_ql/validations/fields_are_defined_on_type_spec.rb +28 -0
  78. data/spec/graph_ql/validations/fields_will_merge_spec.rb +40 -0
  79. data/spec/graph_ql/validations/fragments_are_used_spec.rb +28 -0
  80. data/spec/graph_ql/validator_spec.rb +24 -0
  81. data/spec/spec_helper.rb +2 -2
  82. data/spec/support/dummy_app.rb +123 -63
  83. data/spec/support/dummy_data.rb +11 -0
  84. metadata +107 -59
  85. data/lib/graphql/call.rb +0 -8
  86. data/lib/graphql/connection.rb +0 -65
  87. data/lib/graphql/field.rb +0 -12
  88. data/lib/graphql/field_definer.rb +0 -25
  89. data/lib/graphql/introspection/call_type.rb +0 -13
  90. data/lib/graphql/introspection/connection.rb +0 -9
  91. data/lib/graphql/introspection/field_type.rb +0 -10
  92. data/lib/graphql/introspection/root_call_argument_node.rb +0 -5
  93. data/lib/graphql/introspection/root_call_type.rb +0 -20
  94. data/lib/graphql/introspection/schema_call.rb +0 -8
  95. data/lib/graphql/introspection/schema_type.rb +0 -17
  96. data/lib/graphql/introspection/type_call.rb +0 -8
  97. data/lib/graphql/introspection/type_type.rb +0 -18
  98. data/lib/graphql/node.rb +0 -244
  99. data/lib/graphql/parser/parser.rb +0 -39
  100. data/lib/graphql/parser/transform.rb +0 -22
  101. data/lib/graphql/query.rb +0 -109
  102. data/lib/graphql/root_call.rb +0 -202
  103. data/lib/graphql/root_call_argument.rb +0 -11
  104. data/lib/graphql/root_call_argument_definer.rb +0 -17
  105. data/lib/graphql/schema/all.rb +0 -46
  106. data/lib/graphql/schema/schema.rb +0 -87
  107. data/lib/graphql/schema/schema_validation.rb +0 -32
  108. data/lib/graphql/syntax/call.rb +0 -8
  109. data/lib/graphql/syntax/field.rb +0 -9
  110. data/lib/graphql/syntax/fragment.rb +0 -7
  111. data/lib/graphql/syntax/node.rb +0 -8
  112. data/lib/graphql/syntax/query.rb +0 -8
  113. data/lib/graphql/syntax/variable.rb +0 -7
  114. data/lib/graphql/types/boolean_type.rb +0 -3
  115. data/lib/graphql/types/number_type.rb +0 -3
  116. data/lib/graphql/types/object_type.rb +0 -6
  117. data/lib/graphql/types/string_type.rb +0 -3
  118. data/lib/graphql/version.rb +0 -3
  119. data/spec/graphql/node_spec.rb +0 -69
  120. data/spec/graphql/parser/parser_spec.rb +0 -168
  121. data/spec/graphql/parser/transform_spec.rb +0 -157
  122. data/spec/graphql/query_spec.rb +0 -274
  123. data/spec/graphql/root_call_spec.rb +0 -69
  124. data/spec/graphql/schema/schema_spec.rb +0 -93
  125. data/spec/graphql/schema/schema_validation_spec.rb +0 -48
  126. data/spec/support/nodes.rb +0 -175
@@ -1,18 +0,0 @@
1
- class GraphQL::Introspection::TypeType < GraphQL::Node
2
- exposes "GraphQL::Node"
3
- type "__type__"
4
- field.string(:name)
5
- field.string(:description)
6
- field.introspection_connection(:fields)
7
-
8
- cursor :name
9
-
10
- # they're actually {FieldMapping}s
11
- def fields
12
- target.all_fields.values
13
- end
14
-
15
- def name
16
- schema_name
17
- end
18
- end
@@ -1,244 +0,0 @@
1
- # Node is the base class for your GraphQL nodes.
2
- # It's essentially a delegator that only delegates methods you whitelist with {.field}.
3
- # To use it:
4
- #
5
- # - Extend `GraphQL::Node`
6
- # - Declare what this node will wrap with {.exposes}
7
- # - Declare fields with {.field}
8
- # - Declare calls with {.call}
9
- #
10
- # @example Expose a class in your app
11
- # class PostNode < GraphQL::Node
12
- # exposes('Post')
13
- #
14
- # cursor(:id)
15
- #
16
- # field.number(:id)
17
- # field.string(:title)
18
- # field.string(:content)
19
- # field.connection(:comments)
20
- # end
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
35
- class GraphQL::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
39
- attr_reader :target
40
- # Fields parsed from the query string
41
- attr_reader :syntax_fields
42
- # The query to which this `Node` belongs. Used for accessing its {Query#context}.
43
- attr_reader :query
44
-
45
- def initialize(target=nil, fields:, query:, calls: [])
46
- @query = query
47
- @calls = calls
48
- @syntax_fields = fields
49
- @original_target = target
50
- @target = apply_calls(target)
51
- end
52
-
53
- # If the target responds to `method_name`, send it to target.
54
- def method_missing(method_name, *args, &block)
55
- if target.respond_to?(method_name)
56
- target.public_send(method_name, *args, &block)
57
- else
58
- super
59
- end
60
- end
61
-
62
- # Looks up {#syntax_fields} against this node and returns the results
63
- def 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
84
- end
85
- json
86
- end
87
- end
88
-
89
- # The object passed to {Query#initialize} as `context`.
90
- def context
91
- query.context
92
- end
93
-
94
- def __type__
95
- self.class
96
- end
97
-
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)
111
- end
112
- val
113
- end
114
- end
115
-
116
- class << self
117
- # @param [String] class_name name of the class this node will wrap.
118
- def exposes(*exposes_class_names)
119
- @exposes_class_names = exposes_class_names
120
- GraphQL::SCHEMA.add_type(self)
121
- end
122
-
123
- # The names of the classes wrapped by this node
124
- def exposes_class_names
125
- @exposes_class_names || []
126
- end
127
-
128
- # @param [String] describe
129
- # Provide a description for this node which will be accessible from {SCHEMA}
130
- def desc(describe)
131
- @description = describe
132
- end
133
-
134
- # The description of this node
135
- def description
136
- @description || raise("#{name}.description isn't defined")
137
- end
138
-
139
- # @param [String] type_name
140
- # Declares an alternative name to use in {GraphQL::SCHEMA}
141
- def type(type_name)
142
- @type_name = type_name.to_s
143
- GraphQL::SCHEMA.add_type(self)
144
- end
145
-
146
- # Returns the name of this node used by {GraphQL::SCHEMA}
147
- def schema_name
148
- @type_name || default_schema_name
149
- end
150
-
151
- def default_schema_name
152
- name.split("::").last.sub(/(Node|Type)$/, '').underscore
153
- end
154
-
155
- # @param [String] field_name name of the field to be used as the cursor
156
- # Declares what field will be used as the cursor for this node.
157
- def cursor(field_name)
158
- define_method "cursor" do
159
- field_mapping = self.class.all_fields[field_name.to_s]
160
- public_send(field_mapping.name).to_s
161
- end
162
- end
163
-
164
- # All accessible fields on this node (including those defined in parent classes)
165
- def all_fields
166
- superclass.all_fields.merge(own_fields)
167
- rescue NoMethodError
168
- own_fields
169
- end
170
-
171
- # Fields defined by this class, but not its parents
172
- def own_fields
173
- @own_fields ||= {}
174
- end
175
-
176
- # @return [GraphQL::FieldDefiner] definer
177
- def field
178
- @field_definer ||= GraphQL::FieldDefiner.new(self)
179
- end
180
-
181
- # @param [String] field_name
182
- # Un-define field with name `field_name`
183
- def remove_field(field_name)
184
- own_fields.delete(field_name.to_s)
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
230
- end
231
-
232
- private
233
-
234
- def get_field(syntax_field)
235
- field_mapping = self.class.all_fields[syntax_field.identifier]
236
- if syntax_field.identifier == "cursor"
237
- cursor
238
- elsif field_mapping.nil?
239
- raise GraphQL::FieldNotDefinedError.new(self.class, syntax_field.identifier)
240
- else
241
- field_mapping
242
- end
243
- end
244
- end
@@ -1,39 +0,0 @@
1
- # Parser is a [parslet](http://kschiess.github.io/parslet/) parser for parsing queries.
2
- #
3
- # If it failes to parse, a {SyntaxError} is raised.
4
- class GraphQL::Parser::Parser < Parslet::Parser
5
- root(:query)
6
- rule(:query) { node.repeat.as(:nodes) >> variable.repeat.as(:variables) >> fragment.repeat.as(:fragments) }
7
-
8
- # node
9
- rule(:node) { space? >> call >> space? >> fields.as(:fields) }
10
-
11
- # fragment
12
- rule(:fragment) { space? >> fragment_identifier >> str(":") >> space? >> fields.as(:fields) >> space?}
13
- rule(:fragment_identifier) { (str("$") >> name).as(:identifier) }
14
-
15
- # field set
16
- rule(:fields) { str("{") >> space? >> (field >> separator?).repeat(1) >> space? >> str("}") >> space?}
17
-
18
- # call
19
- rule(:call) { identifier >> str("(") >> (argument.as(:argument) >> separator?).repeat(0).as(:arguments) >> str(")") }
20
- rule(:dot) { str(".") }
21
- rule(:argument) { (identifier | variable_identifier)}
22
-
23
- # field
24
- rule(:field) { (identifier | fragment_identifier) >> call_chain.maybe >> alias_name.maybe >> space? >> fields.as(:fields).maybe }
25
- rule(:call_chain) { (dot >> call).repeat(0).as(:calls) }
26
- rule(:alias_name) { space >> str("as") >> space >> name.as(:alias_name) }
27
-
28
- # variable
29
- rule(:variable) { space? >> variable_identifier >> str(":") >> space? >> (name | json_string ).as(:json_string) >> space?}
30
- rule(:json_string) { str("{") >> (match('[^{}]') | json_string).repeat >> str("}")}
31
- rule(:variable_identifier) { (str("<") >> name >> str(">")).as(:identifier) }
32
-
33
- # general purpose
34
- rule(:separator?) { str(",").maybe >> space? }
35
- rule(:identifier) { name.as(:identifier) }
36
- rule(:name) { match('\w').repeat(1) }
37
- rule(:space) { match('[\s\n]+').repeat(1) }
38
- rule(:space?) { space.maybe }
39
- end
@@ -1,22 +0,0 @@
1
- # {Transform} is a [parslet](http://kschiess.github.io/parslet/) transform for for turning the AST into objects in {GraphQL::Syntax}.
2
- class GraphQL::Parser::Transform < Parslet::Transform
3
- # query
4
- rule(nodes: sequence(:n), variables: sequence(:v), fragments: sequence(:f)) { GraphQL::Syntax::Query.new(nodes: n, variables: v, fragments: f)}
5
- # node
6
- rule(identifier: simple(:i), arguments: sequence(:a), fields: sequence(:f)) {GraphQL::Syntax::Node.new(identifier: i.to_s, arguments: a, fields: f)}
7
- # field
8
- rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c, alias_name: a.to_s)}
9
- rule(identifier: simple(:i), calls: sequence(:c), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c, alias_name: a.to_s)}
10
- rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c)}
11
- rule(identifier: simple(:i), calls: sequence(:c)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c)}
12
- rule(identifier: simple(:i), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, alias_name: a.to_s)}
13
- # call
14
- rule(identifier: simple(:i), arguments: sequence(:a)) { GraphQL::Syntax::Call.new(identifier: i.to_s, arguments: a) }
15
- # argument
16
- rule(argument: simple(:a)) { a.to_s }
17
- rule(identifier: simple(:i)) { i.to_s }
18
- # variable
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)}
22
- end
@@ -1,109 +0,0 @@
1
- # Executes queries from strings against {GraphQL::SCHEMA}.
2
- #
3
- # @example
4
- # query_str = "post(1) { title, comments { count } }"
5
- # query_ctx = {user: current_user}
6
- # query = GraphQL::Query.new(query_str, context: query_ctx)
7
- # result = query.as_result
8
-
9
- class GraphQL::Query
10
- # This is the object passed to {#initialize} as `context:`
11
- attr_reader :context
12
- attr_reader :query_string, :root
13
-
14
- # @param [String] query_string the string to be parsed
15
- # @param [Object] context an object which will be available to all nodes and fields in the schema
16
- def initialize(query_string, context: nil)
17
- if !query_string.is_a?(String) || query_string.length == 0
18
- raise "You must send a query string, not a #{query_string.class.name}"
19
- end
20
- @query_string = query_string
21
- @root = parse(query_string)
22
- @context = context
23
- end
24
-
25
- # @return [Hash] result the JSON response to this query
26
- # Calling {#as_result} more than once won't cause the query to be re-run
27
- def as_result
28
- @as_result ||= execute!
29
- end
30
-
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)
43
- end
44
-
45
- private
46
-
47
- def execute!
48
- root_syntax_node = root.nodes[0]
49
- root_call_class = GraphQL::SCHEMA.get_call(root_syntax_node.identifier)
50
- root_call = root_call_class.new(query: self, syntax_arguments: root_syntax_node.arguments)
51
- result_object = root_call.as_result
52
- return_declarations = root_call_class.return_declarations
53
- result = {}
54
-
55
- if result_object.is_a?(Hash)
56
- result_object.each do |name, value|
57
- node_class = GraphQL::SCHEMA.type_for_object(value)
58
- field_for_node = root_syntax_node.fields.find {|f| f.identifier == name.to_s }
59
- if field_for_node.present?
60
- fields_for_node = field_for_node.fields
61
- node_value = node_class.new(value,query: self, fields: fields_for_node)
62
- result[name.to_s] = node_value.as_result
63
- end
64
- end
65
- elsif result_object.is_a?(Array)
66
- fields_for_node = root_syntax_node.fields
67
- result_object.each do |item|
68
- node_class = GraphQL::SCHEMA.type_for_object(item)
69
- node_value = node_class.new(item, query: self, fields: fields_for_node)
70
- result[node_value.cursor] = node_value.as_result
71
- end
72
- else
73
- node_class = GraphQL::SCHEMA.type_for_object(result_object)
74
- fields_for_node = root_syntax_node.fields
75
- node_value = node_class.new(result_object, query: self, fields: fields_for_node)
76
- result[node_value.cursor] = node_value.as_result
77
- end
78
-
79
- result
80
- end
81
-
82
- def parse(query_string)
83
- parsed_hash = GraphQL::PARSER.parse(query_string)
84
- root_node = GraphQL::TRANSFORM.apply(parsed_hash)
85
- rescue Parslet::ParseFailed => error
86
- line, col = error.cause.source.line_and_column
87
- raise GraphQL::SyntaxError.new(line, col, query_string)
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
109
- end
@@ -1,202 +0,0 @@
1
- # Every query begins with a root call. It might find data or mutate data and return some results.
2
- #
3
- # A root call should:
4
- #
5
- # - declare any arguments with {.argument}, or declare `argument.none`
6
- # - declare returns with {.return}
7
- # - implement {#execute!} to take those arguments and return values
8
- #
9
- # @example
10
- # FindPostCall < GraphQL::RootCall
11
- # argument.number(:ids, any_number: true)
12
- # returns :post
13
- #
14
- # def execute!(*ids)
15
- # ids.map { |id| Post.find(id) }
16
- # end
17
- # end
18
- #
19
- # @example
20
- # CreateCommentCall < GraphQL::RootCall
21
- # argument.number(:post_id)
22
- # argument.object(:comment)
23
- # returns :post, :comment
24
- #
25
- # def execute!(post_id, comment)
26
- # post = Post.find(post_id)
27
- # new_comment = post.comments.create!(comment)
28
- # {
29
- # comment: new_comment,
30
- # post: post,
31
- # }
32
- # end
33
- # end
34
- #
35
- class GraphQL::RootCall
36
- attr_reader :query, :arguments
37
-
38
- # Validates arguments against declared {.argument}s
39
- def initialize(query:, syntax_arguments:)
40
- @query = query
41
-
42
- raise "#{self.class.name} must declare arguments" unless self.class.arguments
43
- @arguments = syntax_arguments.each_with_index.map do |syntax_arg, idx|
44
-
45
- value = if syntax_arg[0] == "<"
46
- query.variables[syntax_arg].json_string
47
- else
48
- syntax_arg
49
- end
50
-
51
- typecast(idx, value)
52
- end
53
- end
54
-
55
- # @param [Array] args (splat) all args provided in query string (as strings)
56
- # This method is invoked with the arguments provided to the query.
57
- # It should do work and return values matching the {.returns} declarations
58
- def execute!(*args)
59
- raise NotImplementedError, "Do work in this method"
60
- end
61
-
62
- # The object passed to {Query#initialize} as `context`
63
- def context
64
- query.context
65
- end
66
-
67
- # Executes the call, validates the return values against declared {.returns}, then returns the return values.
68
- def as_result
69
- return_declarations = self.class.return_declarations
70
- raise "#{self.class.name} must declare returns" unless return_declarations.present?
71
- return_values = execute!(*arguments)
72
-
73
- if return_values.is_a?(Hash)
74
- unexpected_returns = return_values.keys - return_declarations.keys
75
- missing_returns = return_declarations.keys - return_values.keys
76
- if unexpected_returns.any?
77
- raise "#{self.class.name} returned #{unexpected_returns}, but didn't declare them."
78
- elsif missing_returns.any?
79
- raise "#{self.class.name} declared #{missing_returns}, but didn't return them."
80
- end
81
- end
82
- return_values
83
- end
84
-
85
- class << self
86
- # @param [String] ident_name
87
- # Declare an alternative name used in a query string
88
- def indentifier(ident_name)
89
- @identifier = ident_name
90
- GraphQL::SCHEMA.add_call(self)
91
- end
92
-
93
- # The name used by {GraphQL::SCHEMA}. Uses {.identifier} or derives a name from the class name.
94
- def schema_name
95
- @identifier || name.split("::").last.sub(/Call$/, '').underscore
96
- end
97
-
98
- def inherited(child_class)
99
- GraphQL::SCHEMA.add_call(child_class)
100
- end
101
-
102
- # This call won't be visible in `schema()`
103
- def abstract!
104
- GraphQL::SCHEMA.remove_call(self)
105
- end
106
-
107
- # @param [Symbol] return_declarations
108
- # Name of returned values from this call
109
- def returns(*return_declaration_names)
110
- if return_declaration_names.last.is_a?(Hash)
111
- return_declarations_hash = return_declaration_names.pop
112
- else
113
- return_declarations_hash = {}
114
- end
115
-
116
- raise "Return keys must be symbols" if (return_declarations.keys + return_declaration_names).any? { |k| !k.is_a?(Symbol) }
117
-
118
- return_declaration_names.each do |return_sym|
119
- return_type = return_sym.to_s
120
- return_declarations[return_sym] = return_type
121
- end
122
-
123
- return_declarations_hash.each do |return_sym, return_type|
124
- return_declarations[return_sym] = return_type
125
- end
126
- end
127
-
128
- def return_declarations
129
- @return_declarations ||= {}
130
- end
131
-
132
- # @return [GraphQL::RootCallArgumentDefiner] definer
133
- # Use this object to declare arguments. They must be declared in order
134
- # @example
135
- # argument.string("post_title")
136
- # argument.object("comment_data") # allows a JSON object
137
- def argument
138
- @argument ||= GraphQL::RootCallArgumentDefiner.new(self)
139
- end
140
-
141
- def own_arguments
142
- @own_arguments ||= {}
143
- end
144
-
145
- def arguments
146
- superclass.arguments.merge(own_arguments)
147
- rescue NoMethodError
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
170
- end
171
- end
172
-
173
- private
174
-
175
- TYPE_CHECKS = {
176
- "object" => Hash,
177
- "number" => Numeric,
178
- "string" => String,
179
- }
180
-
181
-
182
- def typecast(idx, value)
183
- arg_dec = self.class.argument_at_index(idx)
184
- expected_type = arg_dec.type
185
- expected_type_class = TYPE_CHECKS[expected_type]
186
-
187
- if expected_type == "string"
188
- parsed_value = value
189
- else
190
- parsed_value = JSON.parse('{ "value" : ' + value + '}')["value"]
191
- end
192
-
193
- if !parsed_value.is_a?(expected_type_class)
194
- raise GraphQL::RootCallArgumentError.new(arg_dec, value)
195
- end
196
-
197
- parsed_value
198
- rescue JSON::ParserError
199
- raise GraphQL::RootCallArgumentError.new(arg_dec, value)
200
- end
201
-
202
- end