graphlyte 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4993b6b7dba33932ac40eff10ac57644a120ee41d23df9408d70ea3c5fe12aec
4
- data.tar.gz: f7fba13051b20ad335936347753ec3507b113852bcbdd4c5ad62ac3b127496ec
3
+ metadata.gz: 62efb09535601a1ef60cd1f6d24108a32c954382a8e2f280e8e570bf8d14d0e2
4
+ data.tar.gz: 9098e5efe7b6344f1bff6effddd660de3e4d6632ec5d8c7b18516cdc7d522a4e
5
5
  SHA512:
6
- metadata.gz: 3bab812cb94fd2abeb816180683297c4c95f1486368a5f3029a9917fcf4dcf505d8b6c82777ef4574f550fdc8cd6f0878a92bebcbeb09702b3d832f664e4a0fa
7
- data.tar.gz: cbbfbbff19e0f789d09b01e8581ded8892601b85710b9c9a84ffcc9e393c1ef69aa27c094deb2a81a23078614982519d6ee0f8047ff63d3f1991fddab764a39f
6
+ metadata.gz: 2577fa1470f54149841295630b98b865f44b2cc5059c67a34bdea735dfb10b0da8bedfd325567577b7e0d6b900f9cc5004e4cf7d4b7fe8e6ac801ea491487e49
7
+ data.tar.gz: b319c1970a99b031e13df0b33c74a5687490e423c4d141016cb01edf1fb0e2a5a17474ed19bea4b4db709715a883fb3148fac8414a1da1d293fca52d355d5c7c
@@ -26,15 +26,15 @@ module Graphlyte
26
26
  variables
27
27
  end
28
28
 
29
- def to_h(inner = false)
29
+ def to_h(raw = false)
30
30
  return {} unless values && !values.empty?
31
31
  values.inject({}) do |memo, (k, v)|
32
32
  if v.is_a?(Array)
33
- memo[k.to_s.to_camel_case] = v.map(&:to_s)
33
+ memo[k.to_s.to_camel_case] = v.map { |value| value.to_s(raw) }
34
34
  elsif v.is_a?(Set)
35
35
  memo[k.to_s.to_camel_case] = v.to_h
36
36
  else
37
- memo[k.to_s.to_camel_case] = v.to_s
37
+ memo[k.to_s.to_camel_case] = v.to_s(raw)
38
38
  end
39
39
  memo
40
40
  end
@@ -62,27 +62,17 @@ module Graphlyte
62
62
  end
63
63
 
64
64
  def expand_arguments(data)
65
- data.inject({}) do |memo, (k, v)|
66
- if v.is_a?(Array)
67
- memo[k] = v.map do |item|
68
- if item.is_a?(Value)
69
- item
70
- else
71
- Value.new(item)
72
- end
73
- end
74
- elsif v.is_a?(Hash)
75
- memo[k] = Set.new(v)
65
+ data.transform_values do |value|
66
+ case value
67
+ when Array
68
+ value.map { |item| Value.from(item) }
69
+ when Hash
70
+ Set.new(value)
76
71
  else
77
- if v.is_a?(Value)
78
- memo[k] = v
79
- else
80
- memo[k] = Value.new(v)
81
- end
72
+ Value.from(value)
82
73
  end
83
- memo
84
74
  end
85
75
  end
86
76
  end
87
77
  end
88
- end
78
+ end
@@ -11,6 +11,12 @@ module Graphlyte
11
11
  @value = value
12
12
  end
13
13
 
14
+ def self.from(value)
15
+ return value if value.is_a? self
16
+
17
+ new(value)
18
+ end
19
+
14
20
  def symbol?
15
21
  value.is_a? Symbol
16
22
  end
@@ -19,10 +25,10 @@ module Graphlyte
19
25
  value.is_a? Schema::Types::Base
20
26
  end
21
27
 
22
- def to_s
28
+ def to_s(raw = false)
23
29
  return "$#{value.to_s.to_camel_case}" if value.is_a? Symbol
24
30
  return value if value.is_a? Numeric
25
- return "\"#{value}\"" if value.is_a? String
31
+ return "\"#{value}\"" if value.is_a?(String) && !raw
26
32
  return "null" if value.nil?
27
33
  return "$#{value.placeholder.to_camel_case}" if value.is_a? Schema::Types::Base
28
34
  value.to_s
@@ -8,19 +8,31 @@ 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.class == Fragment ? false : field.name == field_symbol.to_s
22
+ end
23
+
24
+ @fields.select { |field| field.class == Fragment }.each do |fragment|
25
+ fragment.fields.reject! do |field|
26
+ field.name == field_symbol.to_s
27
+ end
28
+ end
17
29
  end
18
30
 
19
31
  def method_missing(method, fieldset_or_hargs=nil, hargs={}, &block)
20
32
  # todo: camel case method
21
33
 
22
34
  # hack for ruby bug in lower versions
23
- if [Fieldset, Fragment].include?(fieldset_or_hargs.class)
35
+ if [Fieldset, Fragment, InlineFragment].include?(fieldset_or_hargs.class)
24
36
  field = Field.new(method, fieldset_or_hargs, hargs)
25
37
  else
26
38
  field = Field.new(method, Fieldset.empty, fieldset_or_hargs)
@@ -0,0 +1,25 @@
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, args: nil)
13
+ # add directive after fieldname?
14
+ string += ' ' * indent unless indent.nil?
15
+ if !args.nil? && args.to_s.empty?
16
+ string += "#{field} " if field
17
+ else
18
+ string += "#{field}#{args.to_s} "
19
+ end
20
+ string += "@#{name}"
21
+ string += @inputs.to_s unless @inputs.to_s.empty?
22
+ string
23
+ end
24
+ end
25
+ 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, args: inputs)
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(0, 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,37 @@
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
+ next if field.class == Fragment
15
+
16
+ modify(field.fieldset.fields, [token], &block)
17
+ field.fieldset.builder.instance_eval(&block) unless field.fieldset.fields.empty?
18
+ end
19
+ else
20
+ needle = fields.find do |field|
21
+ field.name == token
22
+ end
23
+
24
+ raise "#{token} not found in query" unless needle
25
+
26
+ if selector_tokens.size.zero?
27
+ needle.fieldset.builder.instance_eval(&block)
28
+ else
29
+ modify(needle.fieldset.fields, selector_tokens, &block)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
4
35
  class Query < Fieldset
5
36
  using Refinements::StringRefinement
6
37
  attr_reader :name, :type
@@ -11,6 +42,10 @@ module Graphlyte
11
42
  super(**hargs)
12
43
  end
13
44
 
45
+ def at(selector, &block)
46
+ Selector.new(selector).modify(fields, &block)
47
+ end
48
+
14
49
  def placeholders
15
50
  flatten_variables(builder.>>).map do |value|
16
51
  unless value.formal?
@@ -43,7 +78,7 @@ module Graphlyte
43
78
  end
44
79
  str += "(#{type_new.join(", ")})"
45
80
  end
46
- { query: "#{str} #{to_s(1)}", variables: Arguments::Set.new(hargs).to_h }.to_json
81
+ { query: "#{str} #{to_s(1)}", variables: Arguments::Set.new(hargs).to_h(true) }.to_json
47
82
  end
48
83
 
49
84
  def to_s(indent=0)
@@ -62,7 +97,7 @@ module Graphlyte
62
97
  elsif hargs[var.value].is_a? Integer
63
98
  memo << [var.value, "Int"]
64
99
  elsif hargs[var.value].is_a? Array
65
- memo << "[#{merge_variable_types(var.value, hargs).first}]"
100
+ memo << "[#{merge_variable_types(var.value, hargs).first}]"
66
101
  end
67
102
  else
68
103
  memo << [var.value.placeholder, var.value.name, var.value.default]
@@ -70,7 +105,7 @@ module Graphlyte
70
105
  memo
71
106
  end
72
107
  end
73
-
108
+
74
109
  def format_fragments
75
110
  str = "\n"
76
111
  flatten(builder.>>).each do |_, fragment|
@@ -83,8 +118,9 @@ module Graphlyte
83
118
 
84
119
  def flatten_variables(fields, variables=[])
85
120
  fields.each do |field|
86
- variables.concat field.inputs.extract_variables unless field.class.eql?(Fragment)
87
- if field.class.eql?(Fragment)
121
+ variables.concat field.inputs.extract_variables unless [InlineFragment, Fragment].include? field.class
122
+ variables.concat field.directive.inputs.extract_variables if field.respond_to?(:directive) && field.directive
123
+ if [InlineFragment, Fragment].include? field.class
88
124
  flatten_variables(field.fields, variables)
89
125
  else
90
126
  flatten_variables(field.fieldset.fields, variables)
@@ -95,6 +131,7 @@ module Graphlyte
95
131
 
96
132
  def flatten(fields, new_fields = {})
97
133
  fields.each do |field|
134
+ next if field.class == InlineFragment
98
135
  if field.class.eql?(Fragment)
99
136
  new_fields[field.fragment] = field
100
137
  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
@@ -60,24 +92,31 @@ module Graphlyte
60
92
 
61
93
  def parse_arg
62
94
  if (token = expect(:ARG_KEY)) && (value = parse_value)
63
- defaults = parse_default
95
+ parse_default
64
96
  key = token[0][1]
65
- hash = {}
66
- hash[key] = value
67
- hash
68
- elsif (token = expect(:SPECIAL_ARG_KEY)) && (value = parse_value)
69
- defaults = parse_default
70
- @special_args ||= {}
71
97
  arg = {}
72
- if [Array, Hash].include?(value.class)
73
- arg[token[0][1]] = value
74
- else
75
- new_val = Schema::Types::Base.new(value, token[0][1], defaults)
76
- arg[token[0][1]] = new_val
77
- end
78
- @special_args.merge!(arg)
79
- arg
98
+ arg[key] = value
99
+ elsif (token = expect(:SPECIAL_ARG_KEY)) && (value = parse_value)
100
+ arg = expect_and_inflate_special_args(token, value)
101
+ end
102
+
103
+ arg
104
+ end
105
+
106
+ def expect_and_inflate_special_args(token, value)
107
+ return { token[0][1] => value } if value.class == Schema::Types::Base
108
+
109
+ defaults = parse_default
110
+ @special_args ||= {}
111
+ arg = {}
112
+ if [Array, Hash].include?(value.class)
113
+ arg[token[0][1]] = value
114
+ else
115
+ new_val = Schema::Types::Base.new(value, token[0][1], defaults)
116
+ arg[token[0][1]] = new_val
80
117
  end
118
+ @special_args.merge!(arg)
119
+ arg
81
120
  end
82
121
 
83
122
  def parse_value
@@ -89,9 +128,9 @@ module Graphlyte
89
128
  @special_args[ref]
90
129
  elsif token = expect(:SPECIAL_ARG_VAL)
91
130
  token[0][1]
92
- elsif token = expect(:ARG_HASH_START)
131
+ elsif token = expect(:ARG_HASH)
93
132
  parse_arg_hash
94
- elsif expect(:ARG_ARRAY_START)
133
+ elsif expect(:ARG_ARRAY)
95
134
  parse_arg_array
96
135
  end
97
136
  end
@@ -134,6 +173,10 @@ module Graphlyte
134
173
  end
135
174
  end
136
175
 
176
+ def tokens?
177
+ !tokens[position].nil?
178
+ end
179
+
137
180
  def need(*required_tokens)
138
181
  upcoming = tokens[position, required_tokens.size]
139
182
  expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
@@ -154,7 +197,7 @@ module Graphlyte
154
197
  if current_ref
155
198
  exists = sorted.any? do |frags|
156
199
  frags.find do |el|
157
- el[0] == :START_FRAGMENT && el[1] == current_ref[1]
200
+ el[0] == :FRAGMENT && el[1] == current_ref[1]
158
201
  end
159
202
  end
160
203
  if exists
@@ -170,21 +213,33 @@ module Graphlyte
170
213
  end
171
214
  end
172
215
 
173
- def take_fragments
174
- aggregate = @tokens.inject({taking: false, idx: 0, fragments: []}) do |memo, token_arr|
175
- if token_arr[0] == :END_FRAGMENT
176
- memo[:fragments][memo[:idx]] << token_arr
177
- memo[:taking] = false
216
+ # Select the fragments tokens as an array of arrays
217
+ # @return Array [[[:FRAGMENT, 'foo', bar], [:FIELDSET], [:END_FIELDSET]], [[:FRAGMENT 'buzz', 'bazz']...
218
+ def fetch_fragments(tokens = @tokens.dup, fragment_tokens = [], memo = { active: false, starts: 0, ends: 0, idx: 0 })
219
+ token_arr = tokens.shift
220
+ return fragment_tokens if token_arr.nil?
221
+
222
+ if memo[:active] == true
223
+ fragment_tokens[memo[:idx]] << token_arr
224
+ end
225
+
226
+ if token_arr[0] == :END_FIELDSET && memo[:active] == true
227
+ memo[:ends] += 1
228
+ if memo[:starts] == memo[:ends] + 1
229
+ memo[:active] = false
230
+ memo[:ends] = 0
231
+ memo[:starts] = 0
178
232
  memo[:idx] += 1
179
- elsif token_arr[0] === :START_FRAGMENT
180
- memo[:fragments][memo[:idx]] = [token_arr]
181
- memo[:taking] = true
182
- elsif memo[:taking]
183
- memo[:fragments][memo[:idx]] << token_arr
184
233
  end
185
- memo
234
+ elsif token_arr[0] == :FRAGMENT
235
+ memo[:active] = true
236
+ memo[:starts] += 1
237
+ fragment_tokens[memo[:idx]] = [token_arr]
238
+ elsif token_arr[0] == :FIELDSET && memo[:active] == true
239
+ memo[:starts] += 1
186
240
  end
187
- aggregate[:fragments]
241
+
242
+ fetch_fragments(tokens, fragment_tokens, memo)
188
243
  end
189
244
  end
190
245
 
@@ -205,12 +260,16 @@ module Graphlyte
205
260
  end
206
261
 
207
262
  def parse_fragment
208
- if token = expect(:START_FRAGMENT)
263
+ if token = expect(:FRAGMENT)
209
264
  parse_args
210
- builder = Builder.new parse_fields
211
- fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
265
+ if builder = parse_fieldset_into_builder
266
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
267
+ need(:END_FIELDSET) if tokens?
268
+ elsif fields = parse_fields
269
+ builder = Builder.new(fields)
270
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
271
+ end
212
272
  @fragments_dictionary[token[0][1]] = fragment
213
- need(:END_FRAGMENT)
214
273
  end
215
274
  end
216
275
  end
@@ -227,41 +286,42 @@ module Graphlyte
227
286
 
228
287
  def initialize(tokens)
229
288
  @tokens = tokens
230
- @fragment_tokens = sort_fragments([], take_fragments)
289
+
290
+ @fragment_tokens = sort_fragments([], fetch_fragments)
231
291
  @fragments_dictionary = {}
232
292
  @fragments_dictionary = @fragment_tokens.any? ? FragmentParser.new(@fragment_tokens).parse_fragments : {}
233
293
  @position = 0
234
294
  end
235
295
 
236
296
  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])
297
+ if token = expect(:EXPRESSION)
298
+ parse_expression(token[0][1], token[0][2])
299
+ elsif expect(:FRAGMENT)
300
+ skip_fragments
301
+ parse
241
302
  else
242
303
  raise "INVALID"
243
304
  end
244
305
  end
245
306
 
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
307
+ def skip_fragments
308
+ skip_fieldset
252
309
  end
253
310
 
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
311
+ def parse_expression(type, name)
312
+ parse_args
313
+ fields = []
314
+ builder = parse_fieldset_into_builder
315
+ need(:END_FIELDSET)
316
+ query = Query.new(name, type.to_sym, builder: builder)
317
+ query
259
318
  end
260
319
  end
261
320
 
321
+ class LexerError < StandardError; end
322
+
262
323
  class Lexer
263
324
  attr_reader :stack, :scanner
264
-
265
325
  def initialize(gql, scanner: StringScanner.new(gql))
266
326
  @original_string = gql
267
327
  @scanner = scanner
@@ -269,211 +329,283 @@ module Graphlyte
269
329
  end
270
330
 
271
331
  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
332
 
279
333
  def tokenize
280
334
  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
335
+ tokenize_objects
336
+ end
337
+
338
+ @tokens
339
+ end
340
+
341
+ def tokenize_objects
342
+ case state
343
+ when :default # the stack is empty, can only process top level fragments or expressions
344
+ if scanner.scan %r{\s*fragment\s*(\w+)\s*on\s*(\w+)}
345
+ @tokens << [:FRAGMENT, scanner[1], scanner[2]]
346
+ push_context :fragments
347
+ # check for a fieldset
348
+ if scanner.check %r[\s*{]
349
+ tokenize_fieldset
356
350
  else
357
- handle_field
351
+ scanner.scan /\(/
352
+ @tokens << [:START_ARGS]
353
+ push_state :arguments
358
354
  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
- #
355
+ elsif scanner.check /\{/
356
+ @tokens << [:EXPRESSION, 'query', nil] if get_context == :default
357
+ push_context :fieldset
358
+
359
+ tokenize_fieldset
360
+ elsif scanner.scan %r{^(\w+) (\w+)?}
361
+ @tokens << [:EXPRESSION, scanner[1], scanner[2]]
362
+ push_context :expression
363
+ # check for a fieldset
364
+ if scanner.check %r[\s*{]
365
+ tokenize_fieldset
372
366
  else
373
- handle_shared_arguments
367
+ scanner.scan /\(/
368
+ @tokens << [:START_ARGS]
369
+ push_state :arguments
374
370
  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
371
+ elsif scanner.check /\s*\}/
372
+ if get_context == :fragments
373
+ end_fragment
374
+ elsif get_context == :expression
375
+ end_expression
383
376
  end
384
- when :special_args
385
- handle_special_args
377
+ else
378
+ advance
386
379
  end
380
+ when :fieldset
381
+ tokenize_fields
382
+ when :arguments
383
+ tokenize_arguments
384
+ when :argument_defaults
385
+ tokenize_argument_defaults
386
+ when :hash_arguments
387
+ tokenize_hash_arguments
388
+ when :array_arguments
389
+ tokenize_array_arguments
390
+ when :special_args
391
+ tokenize_special_arguments
392
+ when :inline_fragment
393
+ tokenize_inline_fragment
387
394
  end
388
- @tokens
389
395
  end
390
396
 
391
- private
397
+ def check_for_last(regex = /\s*\}/)
398
+ scanner.check regex
399
+ end
392
400
 
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
401
+ def check_for_final
402
+ scanner.check /\s*\}(?!\s*\})/
403
+ end
404
+
405
+ def check_for_not_last
406
+ scanner.check /\s*\}(?=\s*\})/
407
+ end
408
+
409
+ def tokenize_inline_fragment
410
+ if scanner.scan /on (\w+)/
411
+ @tokens << [:MODEL_NAME, scanner[1]]
412
+
413
+ pop_state
414
+ elsif scanner.scan /@(\w+)/
415
+ @tokens << [:DIRECTIVE, scanner[1]]
416
+
417
+ pop_state
419
418
  else
419
+ # throw an error here?
420
420
  advance
421
421
  end
422
422
  end
423
423
 
424
- def handle_shared_arguments
424
+ def end_fieldset
425
+ scanner.scan /\s*\}/
426
+ @tokens << [:END_FIELDSET]
427
+ pop_state
428
+ pop_context if state == :default
429
+ end
430
+
431
+ def end_arguments
432
+ scanner.scan /\s*\)/
433
+ @tokens << [:END_ARGS]
434
+ pop_state
435
+ end_fieldset while check_for_last && state == :fieldset
436
+ end
437
+
438
+ # to tired to figure out why this is right now
439
+ def tokenize_argument_defaults
440
+ if scanner.check /\)/
441
+ @tokens << [:END_DEFAULT_VALUE]
442
+ pop_state
443
+ elsif scanner.scan /[\n|,]/
444
+ @tokens << [:END_DEFAULT_VALUE]
445
+ pop_state
446
+ else
447
+ tokenize_shared_arguments
448
+ end
449
+ end
450
+
451
+ def tokenize_special_arguments
452
+ if scanner.check SPECIAL_ARG_REGEX
453
+ scanner.scan SPECIAL_ARG_REGEX
454
+
455
+ @tokens << [:SPECIAL_ARG_VAL, scanner[1]]
456
+
457
+ pop_state
458
+
459
+ end_arguments if check_for_last(/\s*\)/)
460
+ else
461
+ # revisit this.. should we throw an error here?
462
+ pop_state
463
+ raise LexerError, "why can't we parse #{scanner.peek(5)}"
464
+ end
465
+ end
466
+
467
+ def tokenize_array_arguments
468
+ if scanner.scan /\]/
469
+ @tokens << [:ARG_ARRAY_END]
470
+
471
+ pop_state
472
+ # if check_for_last(')')
473
+ # pop_state
474
+ # end
475
+ else
476
+ tokenize_shared_arguments
477
+ end
478
+ end
479
+
480
+ def tokenize_hash_arguments
481
+ if scanner.scan /\}/
482
+ @tokens << [:ARG_HASH_END]
483
+
484
+ pop_state
485
+ # if this is the last argument in the list, maybe get back to the field scope?
486
+ # if check_for_last(')')
487
+ # pop_state
488
+ # end
489
+ else
490
+ tokenize_shared_arguments
491
+ end
492
+ end
493
+
494
+ def pop_argument_state
495
+ if check_for_last(/\s*\)/)
496
+ end_arguments
497
+ else
498
+ pop_state unless %i[argument_defaults hash_arguments array_arguments special_args arguments].include?(state)
499
+ end
500
+ end
501
+
502
+ def tokenize_arguments
503
+ # pop argument state if arguments are finished
504
+ if scanner.scan %r{\)}
505
+ @tokens << [:END_ARGS]
506
+
507
+ pop_state
508
+ # something(argument: $argument = true)
509
+ elsif scanner.scan %r{=}
510
+ @tokens << [:DEFAULT_VALUE]
511
+
512
+ push_state :argument_defaults
513
+ # noop, should expect this, but not important
514
+ elsif scanner.scan %r{,}
515
+ nil
516
+ else
517
+ tokenize_shared_arguments
518
+ end
519
+ end
520
+
521
+ def tokenize_shared_arguments
425
522
  if scanner.scan /^(\w+):/
426
523
  @tokens << [:ARG_KEY, scanner[1]]
427
- elsif scanner.scan /^\s*\{\s*?/
428
- @tokens << [:ARG_HASH_START]
524
+ elsif scanner.scan %r[{]
525
+ @tokens << [:ARG_HASH]
526
+
429
527
  push_state :hash_arguments
430
- elsif scanner.scan /\s*\[\s*/
431
- @tokens << [:ARG_ARRAY_START]
528
+ elsif scanner.scan /\[/
529
+ @tokens << [:ARG_ARRAY]
530
+
432
531
  push_state :array_arguments
433
- elsif scanner.scan /\s?\"([\w\s]+)\"/
532
+ elsif scanner.scan %r{"(.*?)"}
434
533
  @tokens << [:ARG_STRING_VALUE, scanner[1]]
435
- elsif scanner.scan /\s?(\d+\.\d+)/
534
+
535
+ pop_argument_state
536
+ elsif scanner.scan /(\d+\.\d+)/
436
537
  @tokens << [:ARG_FLOAT_VALUE, scanner[1].to_f]
437
- elsif scanner.scan /\s?(\d+)/
538
+
539
+ pop_argument_state
540
+ elsif scanner.scan /(\d+)/
438
541
  @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]
542
+
543
+ pop_argument_state
544
+ elsif scanner.scan /(true|false)/
545
+ @tokens << [:ARG_BOOL_VALUE, (scanner[1] == 'true')]
546
+
547
+ pop_argument_state
442
548
  elsif scanner.scan /\$(\w+):/
443
549
  @tokens << [:SPECIAL_ARG_KEY, scanner[1]]
550
+
444
551
  push_state :special_args
445
552
  elsif scanner.scan /\$(\w+)/
446
553
  @tokens << [:SPECIAL_ARG_REF, scanner[1]]
447
- else
448
- advance
449
- end
450
- end
451
554
 
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]]
555
+ pop_argument_state
556
+ elsif scanner.scan /,/
557
+ # no-op
558
+ elsif check_for_last(/\s*\)/)
559
+ @tokens << [:END_DEFAULT_VALUE] if state == :argument_defaults
560
+ end_arguments
456
561
  pop_state
457
562
  else
458
- pop_state
563
+ advance
459
564
  end
460
565
  end
461
566
 
462
- def handle_hash_arguments
463
- if scanner.scan /\}/
464
- @tokens << [:ARG_HASH_END]
465
- pop_state
567
+ def tokenize_fields
568
+ if scanner.check %r[{]
569
+ tokenize_fieldset
570
+ # ... on Model - or - ... @directive
571
+ elsif scanner.scan %r{\.{3}\s}
572
+ @tokens << [:INLINE_FRAGMENT]
573
+ push_state :inline_fragment
574
+ # @directive
575
+ elsif scanner.scan %r{@(\w+)}
576
+ @tokens << [:DIRECTIVE, scanner[1]]
577
+
578
+ end_fieldset while check_for_last && state == :fieldset
579
+ # ...fragmentReference (check for last since it is a field literal)
580
+ elsif scanner.scan /\.{3}(\w+)/
581
+ @tokens << [:FRAGMENT_REF, scanner[1]]
582
+
583
+ end_fieldset while check_for_last && state == :fieldset
584
+ # alias:
585
+ elsif scanner.scan %r{(\w+):}
586
+ @tokens << [:ALIAS, scanner[1]]
587
+ # fieldLiteral
588
+ elsif scanner.scan %r{(\w+)}
589
+ @tokens << [:FIELD_NAME, scanner[1]]
590
+
591
+ end_fieldset while check_for_last && state == :fieldset
592
+ # (arguments: true)
593
+ elsif scanner.scan /^\s*\(/
594
+ @tokens << [:START_ARGS]
595
+
596
+ push_state :arguments
466
597
  else
467
- handle_shared_arguments
598
+ advance
468
599
  end
469
600
  end
470
601
 
471
- def handle_array_arguments
472
- if scanner.scan /\s*\]\s*/
473
- @tokens << [:ARG_ARRAY_END]
474
- pop_state
602
+ def tokenize_fieldset
603
+ if scanner.scan %r[\s*{]
604
+ @tokens << [:FIELDSET]
605
+
606
+ push_state :fieldset
475
607
  else
476
- handle_shared_arguments
608
+ raise LexerError, "Expecting `{` got `#{scanner.peek(3)}`"
477
609
  end
478
610
  end
479
611
 
@@ -498,6 +630,10 @@ module Graphlyte
498
630
  end
499
631
 
500
632
  def advance
633
+ unless scanner.check /\s/
634
+ raise LexerError, "Unexpected Char: '#{scanner.peek(20)}'"
635
+ end
636
+
501
637
  scanner.pos = scanner.pos + 1
502
638
  end
503
639
 
@@ -27,6 +27,8 @@ module Graphlyte
27
27
  str << ", " if idx < (value.size - 1)
28
28
  end
29
29
  str << "]"
30
+ elsif value.is_a?(Symbol)
31
+ str << value.to_s
30
32
  else
31
33
  str << "#{Arguments::Value.new(value).to_s}"
32
34
  end
@@ -2,8 +2,8 @@ require_relative "schema/types/base"
2
2
 
3
3
  module Graphlyte
4
4
  class Types
5
- def method_missing(method, placeholder)
6
- Schema::Types::Base.new(method, placeholder)
5
+ def method_missing(method, placeholder, default = nil)
6
+ Schema::Types::Base.new(method, placeholder, default)
7
7
  end
8
8
  end
9
9
  end
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.3
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Gregory
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-12 00:00:00.000000000 Z
11
+ date: 2022-03-27 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