graphlyte 0.2.4 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e01c43639c294e8ae4121522a6b0a5e498e69fd9e4f91e9f3e0a3a04753f826
4
- data.tar.gz: 985c22c79e96547fdb5da497b52b5c2e8d03621f38253847e57afb932e18c752
3
+ metadata.gz: 97c3b9e1d17580b4d94cf3ffaa9f7291d8ff10c9441a9c2270a378daad39f977
4
+ data.tar.gz: f3586e4ea3a92e0ac462b92a17115a81a8c64e6643c296861072a23543ae2b92
5
5
  SHA512:
6
- metadata.gz: 54d9dc438641a2f9f800eff0ade575f893472bbf96faba57e9d69171efbae8a8bd8caf50bdaeb4d7e71aadc37161c61e55c2385a1da98f04d0b687b20b6d654e
7
- data.tar.gz: 671f0580825f19ce0f010926a40a4cdece1ddb408dbda8ec10120c80ad0a2274680fae80085679a1e5f688fc5049e9ad1efae52b21c595ddfa166f2c759fc509
6
+ metadata.gz: a4dd27faa45576150659090362b94f30297faad8a1a1d5758480e63359bd74e92a30960e70fad8aa446c60e1540dd384458590d5f6cc00eb49c9aa09b44d6659
7
+ data.tar.gz: 1887884979b8f92cc8c1b145c782363b7be9e907fc3541cee5e103a4e901d07261f1a6f69aa0aafcce337efda5e5de4a3950fa4803f17a074f4cc6bbd0555ace
@@ -8,19 +8,25 @@ module Graphlyte
8
8
  end
9
9
 
10
10
  def <<(buildable)
11
- raise "Must pass a Fieldset or Fragment" unless [Fragment, Fieldset].include?(buildable.class)
11
+ raise "Must pass a Fieldset or Fragment" unless [Fragment, Fieldset, InlineFragment].include?(buildable.class)
12
12
 
13
13
  @fields.concat(buildable.fields) if buildable.class.eql? Fieldset
14
14
 
15
15
  # todo: handle fragments better, it's not a field
16
- @fields << buildable if buildable.class.eql? Fragment
16
+ @fields << buildable if [InlineFragment, Fragment].include? buildable.class
17
+ end
18
+
19
+ def remove(field_symbol)
20
+ @fields.reject! do |field|
21
+ field.name == field_symbol.to_s
22
+ end
17
23
  end
18
24
 
19
25
  def method_missing(method, fieldset_or_hargs=nil, hargs={}, &block)
20
26
  # todo: camel case method
21
27
 
22
28
  # hack for ruby bug in lower versions
23
- if [Fieldset, Fragment].include?(fieldset_or_hargs.class)
29
+ if [Fieldset, Fragment, InlineFragment].include?(fieldset_or_hargs.class)
24
30
  field = Field.new(method, fieldset_or_hargs, hargs)
25
31
  else
26
32
  field = Field.new(method, Fieldset.empty, fieldset_or_hargs)
@@ -0,0 +1,21 @@
1
+ require_relative 'arguments/set'
2
+
3
+ module Graphlyte
4
+ class Directive
5
+ attr_reader :name, :inputs
6
+
7
+ def initialize(name, **hargs)
8
+ @name = name
9
+ @inputs = Arguments::Set.new(hargs)
10
+ end
11
+
12
+ def inflate(indent, string, field: nil)
13
+ # add directive after fieldname?
14
+ string += ' ' * indent
15
+ string += "#{field} " if field
16
+ string += "@#{name}"
17
+ string += @inputs.to_s unless @inputs.to_s.empty?
18
+ string
19
+ end
20
+ end
21
+ end
@@ -1,25 +1,39 @@
1
1
  require_relative "./arguments/set"
2
+ require_relative 'directive'
2
3
  require_relative "./refinements/string_refinement"
3
4
  module Graphlyte
4
5
  class Field
5
6
  using Refinements::StringRefinement
6
7
 
7
- attr_reader :name, :fieldset, :inputs, :alias
8
+ attr_reader :name, :fieldset, :inputs, :directive
8
9
 
9
- def initialize(name, fieldset, hargs, inputs: Arguments::Set.new(hargs))
10
+ def initialize(name, fieldset, hargs, directive: nil, inputs: Arguments::Set.new(hargs))
10
11
  @name = name.to_s.to_camel_case
11
12
  @fieldset = fieldset
12
13
  @inputs = inputs
13
14
  @alias = nil
15
+ @directive = directive
14
16
  end
15
17
 
16
18
  def atomic?
17
19
  fieldset.empty?
18
- end
20
+ end
19
21
 
20
22
  def alias(name, &block)
21
23
  @alias = name
22
- fieldset.builder.>.instance_eval(&block) if block
24
+ if block
25
+ fieldset.builder.>.instance_eval(&block)
26
+ else
27
+ self
28
+ end
29
+ end
30
+
31
+ def include(**hargs, &block)
32
+ make_directive('include', **hargs, &block)
33
+ end
34
+
35
+ def skip(**hargs, &block)
36
+ make_directive('skip', **hargs, &block)
23
37
  end
24
38
 
25
39
  def to_s(indent=0)
@@ -27,12 +41,25 @@ module Graphlyte
27
41
  actual_indent = ("\s" * indent) * 2
28
42
  if @alias
29
43
  str += "#{actual_indent}#{@alias}: #{name}"
30
- str += inputs.to_s.empty? ? "()" : inputs.to_s
44
+ str += inputs.to_s.empty? ? "()" : inputs.to_s
45
+ elsif @directive
46
+ str = @directive.inflate(indent * 2, str, field: name)
31
47
  else
32
48
  str += "#{actual_indent}#{name}#{inputs.to_s}"
33
49
  end
34
50
  str += " {\n#{fieldset.to_s(indent + 1)}\n#{actual_indent}}" unless atomic?
35
51
  str
36
52
  end
53
+
54
+ private
55
+
56
+ def method_missing(symbol, **hargs, &block)
57
+ make_directive(symbol.to_s, **hargs, &block)
58
+ end
59
+
60
+ def make_directive(name, **hargs, &block)
61
+ @directive = Directive.new(name, **hargs)
62
+ fieldset.builder.>.instance_eval(&block) if block
63
+ end
37
64
  end
38
65
  end
@@ -0,0 +1,29 @@
1
+ require_relative "./fieldset"
2
+
3
+ module Graphlyte
4
+ class InlineFragment < Fieldset
5
+ attr_reader :directive
6
+
7
+ def self.from_directive(directive, **hargs)
8
+ new(nil, directive: directive, **hargs)
9
+ end
10
+
11
+ def initialize(model = nil, directive: nil, **hargs)
12
+ @directive = directive
13
+ super(model, **hargs)
14
+ end
15
+
16
+ def to_s(indent=0)
17
+ actual_indent = ("\s" * indent) * 2
18
+ string = '... '
19
+ string += "on #{model_name}" if model_name
20
+ inflate_indent = model_name ? 1 : 0
21
+ string = directive.inflate(inflate_indent, string) if directive
22
+ string += " {\n"
23
+ string += super(indent + 1)
24
+ string += "\n#{actual_indent}}"
25
+
26
+ "#{actual_indent}#{string}"
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,35 @@
1
1
  require_relative "./refinements/string_refinement"
2
2
  require "json"
3
3
  module Graphlyte
4
+ class Selector
5
+ def initialize(selector)
6
+ @selector_tokens = selector.split('.')
7
+ end
8
+
9
+ def modify(fields, selector_tokens = @selector_tokens, &block)
10
+ token = selector_tokens.shift
11
+
12
+ if token == '*'
13
+ fields.each do |field|
14
+ modify(field.fieldset.fields, [token], &block)
15
+ field.fieldset.builder.instance_eval(&block) unless field.fieldset.fields.empty?
16
+ end
17
+ else
18
+ needle = fields.find do |field|
19
+ field.name == token
20
+ end
21
+
22
+ raise "#{token} not found in query" unless needle
23
+
24
+ if selector_tokens.size.zero?
25
+ needle.fieldset.builder.instance_eval(&block)
26
+ else
27
+ modify(needle.fieldset.fields, selector_tokens, &block)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
4
33
  class Query < Fieldset
5
34
  using Refinements::StringRefinement
6
35
  attr_reader :name, :type
@@ -11,6 +40,10 @@ module Graphlyte
11
40
  super(**hargs)
12
41
  end
13
42
 
43
+ def at(selector, &block)
44
+ Selector.new(selector).modify(fields, &block)
45
+ end
46
+
14
47
  def placeholders
15
48
  flatten_variables(builder.>>).map do |value|
16
49
  unless value.formal?
@@ -83,8 +116,9 @@ module Graphlyte
83
116
 
84
117
  def flatten_variables(fields, variables=[])
85
118
  fields.each do |field|
86
- variables.concat field.inputs.extract_variables unless field.class.eql?(Fragment)
87
- if field.class.eql?(Fragment)
119
+ variables.concat field.inputs.extract_variables unless [InlineFragment, Fragment].include? field.class
120
+ variables.concat field.directive.inputs.extract_variables if field.class == InlineFragment && field.directive
121
+ if [InlineFragment, Fragment].include? field.class
88
122
  flatten_variables(field.fields, variables)
89
123
  else
90
124
  flatten_variables(field.fieldset.fields, variables)
@@ -95,6 +129,7 @@ module Graphlyte
95
129
 
96
130
  def flatten(fields, new_fields = {})
97
131
  fields.each do |field|
132
+ next if field.class == InlineFragment
98
133
  if field.class.eql?(Fragment)
99
134
  new_fields[field.fragment] = field
100
135
  unless field.empty?
@@ -10,11 +10,11 @@ module Graphlyte
10
10
  def to_camel_case
11
11
  start_of_string = match(/(^_+)/)&.[](0)
12
12
  end_of_string = match(/(_+$)/)&.[](0)
13
-
13
+
14
14
  middle = split("_").reject(&:empty?).inject([]) do |memo, str|
15
15
  memo << (memo.empty? ? str : str.capitalize)
16
16
  end.join("")
17
-
17
+
18
18
  "#{start_of_string}#{middle}#{end_of_string}"
19
19
  end
20
20
  end
@@ -13,18 +13,31 @@ module Graphlyte
13
13
  fields
14
14
  end
15
15
 
16
+ def skip_fieldset
17
+ expect(:FIELDSET)
18
+ parse_fields
19
+ need(:END_FIELDSET)
20
+ end
21
+
16
22
  def parse_field
17
23
  alias_field = expect(:ALIAS)
18
24
  if token = expect(:FRAGMENT_REF)
19
25
  raise "Can't find fragment #{token[0][1]}" unless fragments_dictionary[token[0][1]]
20
26
  fragments_dictionary[token[0][1]]
21
- elsif field = expect(:FIELD_NAME)
27
+ elsif expect(:INLINE_FRAGMENT)
28
+ field = parse_inline_fragment
29
+ elsif expect(:FIELDSET)
30
+
31
+ elsif (field = expect(:FIELD_NAME))
22
32
  args = parse_args
23
- if fieldset = parse_fieldset
24
- need(:END_FIELD)
25
- field = Field.new(field[0][1], fieldset, args)
33
+ directive = parse_directive
34
+
35
+ if builder = parse_fieldset_into_builder
36
+ need(:END_FIELDSET)
37
+ fieldset = Fieldset.new(builder: builder)
38
+ field = Field.new(field[0][1], fieldset, args, directive: directive)
26
39
  else
27
- field = Field.new(field[0][1], Fieldset.empty, args)
40
+ field = Field.new(field[0][1], Fieldset.empty, args, directive: directive)
28
41
  end
29
42
 
30
43
  if alias_field
@@ -35,10 +48,29 @@ module Graphlyte
35
48
  end
36
49
  end
37
50
 
38
- def parse_fieldset
39
- if expect(:START_FIELD)
51
+ def parse_inline_fragment
52
+ model_name = expect(:MODEL_NAME)&.dig(0, 1)
53
+ directive = parse_directive
54
+ inputs = directive ? (parse_args || {}) : {}
55
+ fields = expect(:FIELDSET) ? parse_fields : []
56
+ need(:END_FIELDSET)
57
+
58
+ InlineFragment.new(model_name, directive: directive, builder: Builder.new(fields), **inputs)
59
+ end
60
+
61
+ def parse_directive
62
+ if token = expect(:DIRECTIVE)
63
+ inputs = parse_args || {}
64
+
65
+ Directive.new(token[0][1], **inputs)
66
+ end
67
+ end
68
+
69
+ def parse_fieldset_into_builder
70
+ fields = []
71
+ if expect(:FIELDSET)
40
72
  fields = parse_fields
41
- Fieldset.new(builder: Builder.new(fields))
73
+ Builder.new(fields)
42
74
  end
43
75
  end
44
76
 
@@ -51,7 +83,7 @@ module Graphlyte
51
83
  end
52
84
 
53
85
  def parse_default
54
- if expect(:START_DEFAULT_VALUE)
86
+ if expect(:DEFAULT_VALUE)
55
87
  value = parse_value
56
88
  need(:END_DEFAULT_VALUE)
57
89
  value
@@ -89,9 +121,9 @@ module Graphlyte
89
121
  @special_args[ref]
90
122
  elsif token = expect(:SPECIAL_ARG_VAL)
91
123
  token[0][1]
92
- elsif token = expect(:ARG_HASH_START)
124
+ elsif token = expect(:ARG_HASH)
93
125
  parse_arg_hash
94
- elsif expect(:ARG_ARRAY_START)
126
+ elsif expect(:ARG_ARRAY)
95
127
  parse_arg_array
96
128
  end
97
129
  end
@@ -134,6 +166,10 @@ module Graphlyte
134
166
  end
135
167
  end
136
168
 
169
+ def tokens?
170
+ !tokens[position].nil?
171
+ end
172
+
137
173
  def need(*required_tokens)
138
174
  upcoming = tokens[position, required_tokens.size]
139
175
  expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
@@ -154,7 +190,7 @@ module Graphlyte
154
190
  if current_ref
155
191
  exists = sorted.any? do |frags|
156
192
  frags.find do |el|
157
- el[0] == :START_FRAGMENT && el[1] == current_ref[1]
193
+ el[0] == :FRAGMENT && el[1] == current_ref[1]
158
194
  end
159
195
  end
160
196
  if exists
@@ -172,11 +208,11 @@ module Graphlyte
172
208
 
173
209
  def take_fragments
174
210
  aggregate = @tokens.inject({taking: false, idx: 0, fragments: []}) do |memo, token_arr|
175
- if token_arr[0] == :END_FRAGMENT
211
+ if token_arr[0] == :END_FIELDSET
176
212
  memo[:fragments][memo[:idx]] << token_arr
177
213
  memo[:taking] = false
178
214
  memo[:idx] += 1
179
- elsif token_arr[0] === :START_FRAGMENT
215
+ elsif token_arr[0] === :FRAGMENT
180
216
  memo[:fragments][memo[:idx]] = [token_arr]
181
217
  memo[:taking] = true
182
218
  elsif memo[:taking]
@@ -186,6 +222,35 @@ module Graphlyte
186
222
  end
187
223
  aggregate[:fragments]
188
224
  end
225
+
226
+ def fetch_fragments(tokens = @tokens.dup, fragment_tokens = [], memo = { active: false, starts: 0, ends: 0, idx: 0 })
227
+ token_arr = tokens.shift
228
+ return fragment_tokens if token_arr.nil?
229
+
230
+
231
+ if memo[:active] == true
232
+ fragment_tokens[memo[:idx]] << token_arr
233
+ end
234
+
235
+ if token_arr[0] == :END_FIELDSET && memo[:active] == true
236
+ memo[:ends] += 1
237
+ fragment_tokens[memo[:idx]] << token_arr if memo[:starts] == memo[:ends]
238
+
239
+ memo[:active] = false
240
+ memo[:ends] = 0
241
+ memo[:starts] = 0
242
+ memo[:idx] += 1
243
+ elsif token_arr[0] == :FRAGMENT
244
+ memo[:active] = true
245
+ memo[:starts] += 1
246
+ fragment_tokens[memo[:idx]] = [token_arr]
247
+ elsif token_arr[0] == :FIELDSET && memo[:active] == true
248
+ memo[:starts] += 1
249
+ end
250
+
251
+
252
+ fetch_fragments(tokens, fragment_tokens, memo)
253
+ end
189
254
  end
190
255
 
191
256
  class FragmentParser
@@ -205,12 +270,16 @@ module Graphlyte
205
270
  end
206
271
 
207
272
  def parse_fragment
208
- if token = expect(:START_FRAGMENT)
273
+ if token = expect(:FRAGMENT)
209
274
  parse_args
210
- builder = Builder.new parse_fields
211
- fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
275
+ if builder = parse_fieldset_into_builder
276
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
277
+ need(:END_FIELDSET) if tokens?
278
+ elsif fields = parse_fields
279
+ builder = Builder.new(fields)
280
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
281
+ end
212
282
  @fragments_dictionary[token[0][1]] = fragment
213
- need(:END_FRAGMENT)
214
283
  end
215
284
  end
216
285
  end
@@ -227,41 +296,42 @@ module Graphlyte
227
296
 
228
297
  def initialize(tokens)
229
298
  @tokens = tokens
230
- @fragment_tokens = sort_fragments([], take_fragments)
299
+
300
+ @fragment_tokens = sort_fragments([], fetch_fragments)
231
301
  @fragments_dictionary = {}
232
302
  @fragments_dictionary = @fragment_tokens.any? ? FragmentParser.new(@fragment_tokens).parse_fragments : {}
233
303
  @position = 0
234
304
  end
235
305
 
236
306
  def parse
237
- if token = expect(:START_QUERY)
238
- parse_query(token[0][1])
239
- elsif token = expect(:START_MUTATION)
240
- parse_mutation(token[0][1])
307
+ if token = expect(:EXPRESSION)
308
+ parse_expression(token[0][1], token[0][2])
309
+ elsif expect(:FRAGMENT)
310
+ skip_fragments
311
+ parse
241
312
  else
242
313
  raise "INVALID"
243
314
  end
244
315
  end
245
316
 
246
- def parse_query(name)
247
- parse_args
248
- builder = Builder.new parse_fields
249
- query = Query.new(name, :query, builder: builder)
250
- need(:END_QUERY)
251
- query
317
+ def skip_fragments
318
+ skip_fieldset
252
319
  end
253
320
 
254
- def parse_mutation(name)
255
- builder = Builder.new parse_fields
256
- mutation = Query.new(name, :mutation, builder: builder)
257
- need(:END_MUTATION)
258
- mutation
321
+ def parse_expression(type, name)
322
+ parse_args
323
+ fields = []
324
+ builder = parse_fieldset_into_builder
325
+ need(:END_FIELDSET)
326
+ query = Query.new(name, type.to_sym, builder: builder)
327
+ query
259
328
  end
260
329
  end
261
330
 
331
+ class LexerError < StandardError; end
332
+
262
333
  class Lexer
263
334
  attr_reader :stack, :scanner
264
-
265
335
  def initialize(gql, scanner: StringScanner.new(gql))
266
336
  @original_string = gql
267
337
  @scanner = scanner
@@ -269,211 +339,290 @@ module Graphlyte
269
339
  end
270
340
 
271
341
  SPECIAL_ARG_REGEX = /^\s*(?:(?<![\"\{]))([\w\!\[\]]+)(?:(?![\"\}]))/
272
- SIMPLE_EXPRESSION = /(query|mutation|fragment)\s*\w+\s*on\w*.*\{\s*\n*[.|\w\s]*\}/
273
- START_MAP = {
274
- 'query' => :START_QUERY,
275
- 'mutation' => :START_MUTATION,
276
- 'fragment' => :START_FRAGMENT
277
- }
278
342
 
279
343
  def tokenize
280
344
  until scanner.eos?
281
- case state
282
- when :default
283
- if scanner.scan /^query (\w+)/
284
- @tokens << [:START_QUERY, scanner[1]]
285
- push_state :query
286
- elsif scanner.scan /^mutation (\w+)/
287
- @tokens << [:START_MUTATION, scanner[1]]
288
- push_state :mutation
289
- elsif scanner.scan /\s*fragment\s*(\w+)\s*on\s*(\w+)/
290
- @tokens << [:START_FRAGMENT, scanner[1], scanner[2]]
291
- push_state :fragment
292
- elsif scanner.scan /\s*{\s*/
293
- @tokens << [:START_FIELD]
294
- push_state :field
295
- elsif scanner.scan /\s*}\s*/
296
- @tokens << [:END_EXPRESSION_SHOULDNT_GET_THIS]
297
- else
298
- advance
299
- end
300
- when :fragment
301
- if scanner.scan /\s*\}\s*/
302
- @tokens << [:END_FRAGMENT]
303
- pop_state
304
- pop_context
305
- elsif scanner.check /^\s*\{\s*/
306
- if get_context == :field
307
- push_state :field
308
- push_context :field
309
- else
310
- scanner.scan /^\s*\{\s*/
311
- push_context :field
312
- end
313
- else
314
- handle_field
315
- end
316
- when :mutation
317
- if scanner.scan /\}/
318
- @tokens << [:END_MUTATION]
319
- pop_state
320
- pop_context
321
- elsif scanner.check /^\s*\{\s*$/
322
- if get_context == :field
323
- push_state :field
324
- else
325
- scanner.scan /^\s*\{\s*$/
326
- push_context :field
327
- end
328
- else
329
- handle_field
330
- end
331
- when :query
332
- if scanner.scan /\s*\}\s*/
333
- @tokens << [:END_QUERY]
334
- pop_state
335
- pop_context
336
- elsif scanner.check /^\s*\{\s*/
337
- if get_context == :field
338
- push_state :field
339
- push_context :field
340
- else
341
- scanner.scan /^\s*\{\s*/
342
- push_context :field
343
- end
344
- else
345
- handle_field
346
- end
347
- when :field
348
- if scanner.check /\s*\}\s*/
349
- if get_context == :field
350
- scanner.scan /\s*\}\s*/
351
- @tokens << [:END_FIELD]
352
- pop_state
353
- else
354
- pop_state
355
- end
345
+ tokenize_objects
346
+ end
347
+
348
+ @tokens
349
+ end
350
+
351
+ def tokenize_objects
352
+ case state
353
+ when :default # the stack is empty, can only process top level fragments or expressions
354
+ if scanner.scan %r{\s*fragment\s*(\w+)\s*on\s*(\w+)}
355
+ @tokens << [:FRAGMENT, scanner[1], scanner[2]]
356
+ push_context :fragments
357
+ # check for a fieldset
358
+ if scanner.check %r[\s*{]
359
+ tokenize_fieldset
356
360
  else
357
- handle_field
361
+ scanner.scan /\(/
362
+ @tokens << [:START_ARGS]
363
+ push_state :arguments
358
364
  end
359
- when :hash_arguments
360
- handle_hash_arguments
361
- when :array_arguments
362
- handle_array_arguments
363
- when :arguments
364
- if scanner.scan /\s*\)\s*/
365
- @tokens << [:END_ARGS]
366
- pop_state
367
- elsif scanner.scan /\=/
368
- @tokens << [:START_DEFAULT_VALUE]
369
- push_state :argument_defaults
370
- elsif scanner.scan /,/
371
- #
365
+ elsif scanner.check /\{/
366
+ push_context :fieldset
367
+
368
+ tokenize_fieldset
369
+ elsif scanner.scan %r{^(\w+) (\w+)}
370
+ @tokens << [:EXPRESSION, scanner[1], scanner[2]]
371
+ push_context :expression
372
+ # check for a fieldset
373
+ if scanner.check %r[\s*{]
374
+ tokenize_fieldset
372
375
  else
373
- handle_shared_arguments
376
+ scanner.scan /\(/
377
+ @tokens << [:START_ARGS]
378
+ push_state :arguments
374
379
  end
375
- when :argument_defaults
376
- if @stack.reverse.take(2).eql?([:argument_defaults, :argument_defaults])
377
- @tokens << [:END_DEFAULT_VALUE]
378
- pop_state
379
- pop_state
380
- else
381
- push_state :argument_defaults
382
- handle_shared_arguments
380
+ elsif scanner.check /\s*\}/
381
+ if get_context == :fragments
382
+ end_fragment
383
+ elsif get_context == :expression
384
+ end_expression
383
385
  end
384
- when :special_args
385
- handle_special_args
386
+ else
387
+ advance
386
388
  end
389
+ when :fieldset
390
+ tokenize_fields
391
+ when :arguments
392
+ tokenize_arguments
393
+ when :argument_defaults
394
+ tokenize_shared_arguments
395
+ when :hash_arguments
396
+ tokenize_hash_arguments
397
+ when :array_arguments
398
+ tokenize_array_arguments
399
+ when :special_args
400
+ tokenize_special_arguments
401
+ when :inline_fragment
402
+ tokenize_inline_fragment
387
403
  end
388
- @tokens
389
404
  end
390
405
 
391
- private
406
+ def check_for_last(regex = /\s*\}/)
407
+ scanner.check regex
408
+ end
392
409
 
393
- def handle_field
394
- if scanner.scan /\s*\{\s*/
395
- @context = :field
396
- @tokens << [:START_FIELD]
397
- push_state :field
398
- elsif scanner.check /\.{3}(\w+)\s*\}/
399
- scanner.scan /\.{3}(\w+)/
400
- @tokens << [:FRAGMENT_REF, scanner[1]]
401
- pop_context
402
- # we need to pop state if we are nested in a field, and not in the query context
403
- pop_state if get_context == :field
404
- elsif scanner.scan /\.{3}(\w+)/
405
- @tokens << [:FRAGMENT_REF, scanner[1]]
406
- elsif scanner.scan /\s*(\w+):\s*/
407
- @tokens << [:ALIAS, scanner[1]]
408
- elsif scanner.check /\s*(\w+)\s*\}/
409
- scanner.scan /\s*(\w+)\s*/
410
- @tokens << [:FIELD_NAME, scanner[1]]
411
- pop_context
412
- # we need to pop state if we are nested in a field, and not in the query context
413
- pop_state if get_context == :field
414
- elsif scanner.scan /\s*(\w+)\s*/
415
- @tokens << [:FIELD_NAME, scanner[1]]
416
- elsif scanner.scan /^\s*\(/
417
- @tokens << [:START_ARGS]
418
- push_state :arguments
410
+ def check_for_final
411
+ scanner.check /\s*\}(?!\s*\})/
412
+ end
413
+
414
+ def check_for_not_last
415
+ scanner.check /\s*\}(?=\s*\})/
416
+ end
417
+
418
+ def tokenize_inline_fragment
419
+ if scanner.scan /on (\w+)/
420
+ @tokens << [:MODEL_NAME, scanner[1]]
421
+
422
+ pop_state
423
+ elsif scanner.scan /@(\w+)/
424
+ @tokens << [:DIRECTIVE, scanner[1]]
425
+
426
+ pop_state
419
427
  else
428
+ # throw an error here?
420
429
  advance
421
430
  end
422
431
  end
423
432
 
424
- def handle_shared_arguments
433
+ def end_fieldset
434
+ scanner.scan /\s*\}/
435
+ @tokens << [:END_FIELDSET]
436
+ pop_state
437
+ end
438
+
439
+ def end_arguments
440
+ scanner.scan /\s*\)/
441
+ @tokens << [:END_ARGS]
442
+ pop_state
443
+ end
444
+
445
+ def end_fragment
446
+ scanner.scan /\s*\}/
447
+ @tokens << [:END_FRAGMENT]
448
+ pop_state
449
+ pop_context
450
+ end
451
+
452
+ def end_expression
453
+ scanner.scan /\s*\}/
454
+ @tokens << [:END_EXPRESSION]
455
+ pop_state
456
+ pop_context
457
+ end
458
+
459
+ # to tired to figure out why this is right now
460
+ def tokenize_argument_defaults
461
+ if scanner.scan /\)/
462
+ @tokens << [:END_DEFAULT_VALUE]
463
+ pop_state
464
+ else
465
+ tokenize_shared_arguments
466
+ end
467
+ end
468
+
469
+ def tokenize_special_arguments
470
+ if scanner.check SPECIAL_ARG_REGEX
471
+ scanner.scan SPECIAL_ARG_REGEX
472
+
473
+ @tokens << [:SPECIAL_ARG_VAL, scanner[1]]
474
+
475
+ pop_state
476
+
477
+ end_arguments if check_for_last(/\s*\)/)
478
+ else
479
+ # revisit this.. should we throw an error here?
480
+ pop_state
481
+ raise LexerError, "why can't we parse #{scanner.peek(5)}"
482
+ end
483
+ end
484
+
485
+ def tokenize_array_arguments
486
+ if scanner.scan /\]/
487
+ @tokens << [:ARG_ARRAY_END]
488
+
489
+ pop_state
490
+ # if check_for_last(')')
491
+ # pop_state
492
+ # end
493
+ else
494
+ tokenize_shared_arguments
495
+ end
496
+ end
497
+
498
+ def tokenize_hash_arguments
499
+ if scanner.scan /\}/
500
+ @tokens << [:ARG_HASH_END]
501
+
502
+ pop_state
503
+ # if this is the last argument in the list, maybe get back to the field scope?
504
+ # if check_for_last(')')
505
+ # pop_state
506
+ # end
507
+ else
508
+ tokenize_shared_arguments
509
+ end
510
+ end
511
+
512
+ def tokenize_arguments
513
+ # pop argument state if arguments are finished
514
+ if scanner.scan %r{\)}
515
+ @tokens << [:END_ARGS]
516
+
517
+ pop_state
518
+ # something(argument: $argument = true)
519
+ # ^
520
+ elsif scanner.scan %r{=}
521
+ @tokens << [:DEFAULT_VALUE]
522
+
523
+ push_state :argument_defaults
524
+ # noop, should expect this, but not important
525
+ elsif scanner.scan %r{,}
526
+ nil
527
+ else
528
+ tokenize_shared_arguments
529
+ end
530
+ end
531
+
532
+ def tokenize_shared_arguments
425
533
  if scanner.scan /^(\w+):/
426
534
  @tokens << [:ARG_KEY, scanner[1]]
427
- elsif scanner.scan /^\s*\{\s*?/
428
- @tokens << [:ARG_HASH_START]
535
+ elsif scanner.scan %r[{]
536
+ @tokens << [:ARG_HASH]
537
+
429
538
  push_state :hash_arguments
430
- elsif scanner.scan /\s*\[\s*/
431
- @tokens << [:ARG_ARRAY_START]
539
+ elsif scanner.scan /\[/
540
+ @tokens << [:ARG_ARRAY]
541
+
432
542
  push_state :array_arguments
433
- elsif scanner.scan /\s?\"([\w\s]+)\"/
543
+ elsif scanner.scan %r{"(.*?)"}
434
544
  @tokens << [:ARG_STRING_VALUE, scanner[1]]
435
- elsif scanner.scan /\s?(\d+\.\d+)/
545
+
546
+ end_arguments if check_for_last(/\s*\)/)
547
+ elsif scanner.scan /(\d+\.\d+)/
436
548
  @tokens << [:ARG_FLOAT_VALUE, scanner[1].to_f]
437
- elsif scanner.scan /\s?(\d+)/
549
+
550
+ end_arguments if check_for_last(/\s*\)/)
551
+ elsif scanner.scan /(\d+)/
438
552
  @tokens << [:ARG_NUM_VALUE, scanner[1].to_i]
439
- elsif scanner.scan /\s?(true|false)\s?/
440
- bool = scanner[1] == "true"
441
- @tokens << [:ARG_BOOL_VALUE, bool]
553
+
554
+ end_arguments if check_for_last(/\s*\)/)
555
+ elsif scanner.scan /(true|false)/
556
+ @tokens << [:ARG_BOOL_VALUE, (scanner[1] == 'true')]
557
+
558
+ end_arguments if check_for_last(/\s*\)/)
442
559
  elsif scanner.scan /\$(\w+):/
443
560
  @tokens << [:SPECIAL_ARG_KEY, scanner[1]]
561
+
444
562
  push_state :special_args
445
563
  elsif scanner.scan /\$(\w+)/
446
564
  @tokens << [:SPECIAL_ARG_REF, scanner[1]]
447
- else
448
- advance
449
- end
450
- end
451
565
 
452
- def handle_special_args
453
- if scanner.check SPECIAL_ARG_REGEX
454
- scanner.scan SPECIAL_ARG_REGEX
455
- @tokens << [:SPECIAL_ARG_VAL, scanner[1]]
566
+ end_arguments if check_for_last(/\s*\)/)
567
+ elsif scanner.scan /,/
568
+ # no-op
569
+ elsif check_for_last(/\s*\)/)
570
+ @tokens << [:END_DEFAULT_VALUE] if state == :argument_defaults
571
+ end_arguments
456
572
  pop_state
457
573
  else
458
- pop_state
574
+ advance
459
575
  end
460
576
  end
461
577
 
462
- def handle_hash_arguments
463
- if scanner.scan /\}/
464
- @tokens << [:ARG_HASH_END]
465
- pop_state
578
+ def tokenize_fields
579
+ if scanner.check %r[{]
580
+ tokenize_fieldset
581
+ # ... on Model - or - ... @directive
582
+ elsif scanner.scan %r{\.{3}\s}
583
+ @tokens << [:INLINE_FRAGMENT]
584
+ push_state :inline_fragment
585
+ # @directive
586
+ elsif scanner.scan %r{@(\w+)}
587
+ @tokens << [:DIRECTIVE, scanner[1]]
588
+ # ...fragmentReference (check for last since it is a field literal)
589
+ elsif scanner.scan /\.{3}(\w+)/
590
+ @tokens << [:FRAGMENT_REF, scanner[1]]
591
+
592
+ end_fieldset while check_for_last && state == :fieldset
593
+ # alias:
594
+ elsif scanner.scan %r{(\w+):}
595
+ @tokens << [:ALIAS, scanner[1]]
596
+ # fieldLiteral
597
+ elsif scanner.scan %r{(\w+)}
598
+ @tokens << [:FIELD_NAME, scanner[1]]
599
+
600
+ end_fieldset while check_for_last && state == :fieldset
601
+ # (arguments: true)
602
+ elsif scanner.scan /^\s*\(/
603
+ @tokens << [:START_ARGS]
604
+
605
+ push_state :arguments
606
+ elsif check_for_final
607
+ if get_context == :fragments
608
+ end_fragment
609
+ elsif get_context == :expression
610
+ end_expression
611
+ else
612
+ advance
613
+ end
466
614
  else
467
- handle_shared_arguments
615
+ advance
468
616
  end
469
617
  end
470
618
 
471
- def handle_array_arguments
472
- if scanner.scan /\s*\]\s*/
473
- @tokens << [:ARG_ARRAY_END]
474
- pop_state
619
+ def tokenize_fieldset
620
+ if scanner.scan %r[\s*{]
621
+ @tokens << [:FIELDSET]
622
+
623
+ push_state :fieldset
475
624
  else
476
- handle_shared_arguments
625
+ raise LexerError, "Expecting `{` got `#{scanner.peek(3)}`"
477
626
  end
478
627
  end
479
628
 
@@ -498,6 +647,10 @@ module Graphlyte
498
647
  end
499
648
 
500
649
  def advance
650
+ unless scanner.check /\s/
651
+ raise LexerError, "Unexpected Char: '#{scanner.peek(3)}'"
652
+ end
653
+
501
654
  scanner.pos = scanner.pos + 1
502
655
  end
503
656
 
data/lib/graphlyte.rb CHANGED
@@ -2,13 +2,14 @@ require 'json'
2
2
  require_relative "./graphlyte/fieldset"
3
3
  require_relative "./graphlyte/query"
4
4
  require_relative "./graphlyte/fragment"
5
+ require_relative 'graphlyte/inline_fragment'
5
6
  require_relative "./graphlyte/schema_query"
6
7
  require_relative "./graphlyte/types"
7
8
  require_relative "./graphlyte/schema/parser"
8
9
 
9
10
  module Graphlyte
10
11
  extend SchemaQuery
11
-
12
+
12
13
  TYPES = Types.new
13
14
 
14
15
  def self.parse(gql)
@@ -23,6 +24,18 @@ module Graphlyte
23
24
  Query.new(name, :mutation, builder: build(&block))
24
25
  end
25
26
 
27
+ def self.custom(name, type, &block)
28
+ Query.new(name, type.to_sym, builder: build(&block))
29
+ end
30
+
31
+ def self.inline_fragment(model_name, &block)
32
+ InlineFragment.new(model_name, builder: build(&block))
33
+ end
34
+
35
+ def self.inline_directive(directive, **hargs, &block)
36
+ InlineFragment.from_directive(directive, **hargs, builder: build(&block) )
37
+ end
38
+
26
39
  def self.fragment(fragment_name, model_name, &block)
27
40
  Fragment.new(fragment_name, model_name, builder: build(&block))
28
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphlyte
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Gregory
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-24 00:00:00.000000000 Z
11
+ date: 2022-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -48,9 +48,11 @@ files:
48
48
  - lib/graphlyte/arguments/set.rb
49
49
  - lib/graphlyte/arguments/value.rb
50
50
  - lib/graphlyte/builder.rb
51
+ - lib/graphlyte/directive.rb
51
52
  - lib/graphlyte/field.rb
52
53
  - lib/graphlyte/fieldset.rb
53
54
  - lib/graphlyte/fragment.rb
55
+ - lib/graphlyte/inline_fragment.rb
54
56
  - lib/graphlyte/query.rb
55
57
  - lib/graphlyte/refinements/string_refinement.rb
56
58
  - lib/graphlyte/schema/parser.rb