graphlyte 0.2.4 → 0.3.2

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: 5e01c43639c294e8ae4121522a6b0a5e498e69fd9e4f91e9f3e0a3a04753f826
4
- data.tar.gz: 985c22c79e96547fdb5da497b52b5c2e8d03621f38253847e57afb932e18c752
3
+ metadata.gz: 93f97fef094a43b1c151dee58329d7b198210246b1fec046e18263d65437d3f4
4
+ data.tar.gz: 3d057d189d9a93e601c7b91ce5ad1feb1fd00b0ab17e1c772ebdbff58cbe8349
5
5
  SHA512:
6
- metadata.gz: 54d9dc438641a2f9f800eff0ade575f893472bbf96faba57e9d69171efbae8a8bd8caf50bdaeb4d7e71aadc37161c61e55c2385a1da98f04d0b687b20b6d654e
7
- data.tar.gz: 671f0580825f19ce0f010926a40a4cdece1ddb408dbda8ec10120c80ad0a2274680fae80085679a1e5f688fc5049e9ad1efae52b21c595ddfa166f2c759fc509
6
+ metadata.gz: 8f106a673e0d70b97d0a4d5634c5c5b03c540fa4e4b857465e2da4252053744f2950e1b78021b99fd0ff3a4fb7bdd896aedbe775a9afc5c2e50b9017692dc626
7
+ data.tar.gz: 5ce5c740e6b25fca56736261db1596e25471f1e7bf5bd91785f81da8d2d8d6641b75487b325063ff115a2a3594c6f3fb891e47613c93e2ea5d53529240251b06
@@ -12,6 +12,22 @@ module Graphlyte
12
12
  @values = expand_arguments(data) unless data.nil?
13
13
  end
14
14
 
15
+ def resolve_lazy_special_args(parser_special_args)
16
+ @values&.each do |key, value|
17
+ if value.is_a?(Set)
18
+ value.resolve_lazy_special_args(parser_special_args) if value.is_a?(Set)
19
+ elsif value.is_a?(Array)
20
+ value.each do |it|
21
+ it.refresh(parser_special_args) if it.is_a?(Value)
22
+ it.resolve_lazy_special_args(parser_special_args) if it.is_a?(Set)
23
+ end
24
+ else
25
+ value.refresh(parser_special_args)
26
+ end
27
+ [key, value]
28
+ end.to_h
29
+ end
30
+
15
31
  def extract_variables(values=@values, variables=[])
16
32
  values&.each do |key, value|
17
33
  if value.is_a?(Set)
@@ -62,27 +78,17 @@ module Graphlyte
62
78
  end
63
79
 
64
80
  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)
81
+ data.transform_values do |value|
82
+ case value
83
+ when Array
84
+ value.map { |item| Value.from(item) }
85
+ when Hash
86
+ Set.new(value)
76
87
  else
77
- if v.is_a?(Value)
78
- memo[k] = v
79
- else
80
- memo[k] = Value.new(v)
81
- end
88
+ Value.from(value)
82
89
  end
83
- memo
84
90
  end
85
91
  end
86
92
  end
87
93
  end
88
- end
94
+ 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,6 +25,10 @@ module Graphlyte
19
25
  value.is_a? Schema::Types::Base
20
26
  end
21
27
 
28
+ def refresh(args)
29
+ @value = value.call(args) if value.is_a? Proc
30
+ end
31
+
22
32
  def to_s(raw = false)
23
33
  return "$#{value.to_s.to_camel_case}" if value.is_a? Symbol
24
34
  return value if value.is_a? Numeric
@@ -0,0 +1,17 @@
1
+ module Graphlyte
2
+ module Arguments
3
+ class ValueLiteral
4
+ attr_reader :value
5
+
6
+ def initialize(string)
7
+ raise 'Value must be a string' unless string.class == String
8
+
9
+ @value = string
10
+ end
11
+
12
+ def to_s
13
+ @value
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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?
@@ -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]
@@ -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
@@ -4,6 +4,7 @@ require_relative "../query"
4
4
  require_relative "../fragment"
5
5
  require_relative "../schema_query"
6
6
  require_relative "../types"
7
+ require_relative "../arguments/value_literal"
7
8
 
8
9
  module Graphlyte
9
10
  module Schema
@@ -13,18 +14,31 @@ module Graphlyte
13
14
  fields
14
15
  end
15
16
 
17
+ def skip_fieldset
18
+ expect(:FIELDSET)
19
+ parse_fields
20
+ need(:END_FIELDSET)
21
+ end
22
+
16
23
  def parse_field
17
24
  alias_field = expect(:ALIAS)
18
25
  if token = expect(:FRAGMENT_REF)
19
26
  raise "Can't find fragment #{token[0][1]}" unless fragments_dictionary[token[0][1]]
20
27
  fragments_dictionary[token[0][1]]
21
- elsif field = expect(:FIELD_NAME)
28
+ elsif expect(:INLINE_FRAGMENT)
29
+ field = parse_inline_fragment
30
+ elsif expect(:FIELDSET)
31
+
32
+ elsif (field = expect(:FIELD_NAME))
22
33
  args = parse_args
23
- if fieldset = parse_fieldset
24
- need(:END_FIELD)
25
- field = Field.new(field[0][1], fieldset, args)
34
+ directive = parse_directive
35
+
36
+ if builder = parse_fieldset_into_builder
37
+ need(:END_FIELDSET)
38
+ fieldset = Fieldset.new(builder: builder)
39
+ field = Field.new(field[0][1], fieldset, args, directive: directive)
26
40
  else
27
- field = Field.new(field[0][1], Fieldset.empty, args)
41
+ field = Field.new(field[0][1], Fieldset.empty, args, directive: directive)
28
42
  end
29
43
 
30
44
  if alias_field
@@ -35,10 +49,29 @@ module Graphlyte
35
49
  end
36
50
  end
37
51
 
38
- def parse_fieldset
39
- if expect(:START_FIELD)
52
+ def parse_inline_fragment
53
+ model_name = expect(:MODEL_NAME)&.dig(0, 1)
54
+ directive = parse_directive
55
+ inputs = directive ? (parse_args || {}) : {}
56
+ fields = expect(:FIELDSET) ? parse_fields : []
57
+ need(:END_FIELDSET)
58
+
59
+ InlineFragment.new(model_name, directive: directive, builder: Builder.new(fields), **inputs)
60
+ end
61
+
62
+ def parse_directive
63
+ if token = expect(:DIRECTIVE)
64
+ inputs = parse_args || {}
65
+
66
+ Directive.new(token[0][1], **inputs)
67
+ end
68
+ end
69
+
70
+ def parse_fieldset_into_builder
71
+ fields = []
72
+ if expect(:FIELDSET)
40
73
  fields = parse_fields
41
- Fieldset.new(builder: Builder.new(fields))
74
+ Builder.new(fields)
42
75
  end
43
76
  end
44
77
 
@@ -51,7 +84,7 @@ module Graphlyte
51
84
  end
52
85
 
53
86
  def parse_default
54
- if expect(:START_DEFAULT_VALUE)
87
+ if expect(:DEFAULT_VALUE)
55
88
  value = parse_value
56
89
  need(:END_DEFAULT_VALUE)
57
90
  value
@@ -60,38 +93,55 @@ module Graphlyte
60
93
 
61
94
  def parse_arg
62
95
  if (token = expect(:ARG_KEY)) && (value = parse_value)
63
- defaults = parse_default
96
+ parse_default
64
97
  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
98
  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
99
+ arg[key] = value
100
+ elsif (token = expect(:SPECIAL_ARG_KEY)) && (value = parse_value)
101
+ arg = expect_and_inflate_special_args(token, value)
102
+ end
103
+
104
+ arg
105
+ end
106
+
107
+ def expect_and_inflate_special_args(token, value)
108
+ return { token[0][1] => value } if value.class == Schema::Types::Base
109
+
110
+ defaults = parse_default
111
+ @special_args ||= {}
112
+ arg = {}
113
+ if [Array, Hash].include?(value.class)
114
+ arg[token[0][1]] = value
115
+ else
116
+ new_val = Schema::Types::Base.new(value, token[0][1], defaults)
117
+ arg[token[0][1]] = new_val
80
118
  end
119
+ @special_args.merge!(arg)
120
+ arg
81
121
  end
82
122
 
83
123
  def parse_value
84
124
  if token = expect(:ARG_NUM_VALUE) || expect(:ARG_STRING_VALUE) || expect(:ARG_BOOL_VALUE) || expect(:ARG_FLOAT_VALUE)
85
125
  token[0][1]
126
+ elsif token = expect(:ARG_LITERAL_VALUE)
127
+ Graphlyte::Arguments::ValueLiteral.new(token[0][1])
86
128
  elsif token = expect(:SPECIAL_ARG_REF)
87
129
  ref = token[0][1]
88
- raise "Can't find ref $#{ref}" unless @special_args[ref]
89
- @special_args[ref]
130
+ # can't prove if this exists yet, so lets add it to the list
131
+ unless @special_args&.dig(ref)
132
+ @refs_to_validate ||= []
133
+ @refs_to_validate << ref
134
+ -> (args) do
135
+ args[ref]
136
+ end
137
+ else
138
+ @special_args[ref]
139
+ end
90
140
  elsif token = expect(:SPECIAL_ARG_VAL)
91
141
  token[0][1]
92
- elsif token = expect(:ARG_HASH_START)
142
+ elsif token = expect(:ARG_HASH)
93
143
  parse_arg_hash
94
- elsif expect(:ARG_ARRAY_START)
144
+ elsif expect(:ARG_ARRAY)
95
145
  parse_arg_array
96
146
  end
97
147
  end
@@ -134,6 +184,10 @@ module Graphlyte
134
184
  end
135
185
  end
136
186
 
187
+ def tokens?
188
+ !tokens[position].nil?
189
+ end
190
+
137
191
  def need(*required_tokens)
138
192
  upcoming = tokens[position, required_tokens.size]
139
193
  expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
@@ -154,7 +208,7 @@ module Graphlyte
154
208
  if current_ref
155
209
  exists = sorted.any? do |frags|
156
210
  frags.find do |el|
157
- el[0] == :START_FRAGMENT && el[1] == current_ref[1]
211
+ el[0] == :FRAGMENT && el[1] == current_ref[1]
158
212
  end
159
213
  end
160
214
  if exists
@@ -170,26 +224,38 @@ module Graphlyte
170
224
  end
171
225
  end
172
226
 
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
227
+ # Select the fragments tokens as an array of arrays
228
+ # @return Array [[[:FRAGMENT, 'foo', bar], [:FIELDSET], [:END_FIELDSET]], [[:FRAGMENT 'buzz', 'bazz']...
229
+ def fetch_fragments(tokens = @tokens.dup, fragment_tokens = [], memo = { active: false, starts: 0, ends: 0, idx: 0 })
230
+ token_arr = tokens.shift
231
+ return fragment_tokens if token_arr.nil?
232
+
233
+ if memo[:active] == true
234
+ fragment_tokens[memo[:idx]] << token_arr
235
+ end
236
+
237
+ if token_arr[0] == :END_FIELDSET && memo[:active] == true
238
+ memo[:ends] += 1
239
+ if memo[:starts] == memo[:ends] + 1
240
+ memo[:active] = false
241
+ memo[:ends] = 0
242
+ memo[:starts] = 0
178
243
  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
244
  end
185
- memo
245
+ elsif token_arr[0] == :FRAGMENT
246
+ memo[:active] = true
247
+ memo[:starts] += 1
248
+ fragment_tokens[memo[:idx]] = [token_arr]
249
+ elsif token_arr[0] == :FIELDSET && memo[:active] == true
250
+ memo[:starts] += 1
186
251
  end
187
- aggregate[:fragments]
252
+
253
+ fetch_fragments(tokens, fragment_tokens, memo)
188
254
  end
189
255
  end
190
256
 
191
257
  class FragmentParser
192
- attr_reader :tokens, :position, :fragments_dictionary
258
+ attr_reader :tokens, :position, :fragments_dictionary, :special_args
193
259
 
194
260
  include ParserHelpers
195
261
 
@@ -205,12 +271,16 @@ module Graphlyte
205
271
  end
206
272
 
207
273
  def parse_fragment
208
- if token = expect(:START_FRAGMENT)
274
+ if token = expect(:FRAGMENT)
209
275
  parse_args
210
- builder = Builder.new parse_fields
211
- fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
276
+ if builder = parse_fieldset_into_builder
277
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
278
+ need(:END_FIELDSET) if tokens?
279
+ elsif fields = parse_fields
280
+ builder = Builder.new(fields)
281
+ fragment = Fragment.new(token[0][1], token[0][2], builder: builder)
282
+ end
212
283
  @fragments_dictionary[token[0][1]] = fragment
213
- need(:END_FRAGMENT)
214
284
  end
215
285
  end
216
286
  end
@@ -227,41 +297,61 @@ module Graphlyte
227
297
 
228
298
  def initialize(tokens)
229
299
  @tokens = tokens
230
- @fragment_tokens = sort_fragments([], take_fragments)
300
+
301
+ @fragment_tokens = sort_fragments([], fetch_fragments)
231
302
  @fragments_dictionary = {}
232
303
  @fragments_dictionary = @fragment_tokens.any? ? FragmentParser.new(@fragment_tokens).parse_fragments : {}
233
304
  @position = 0
234
305
  end
235
306
 
307
+ def refresh_lazy_refs(fields)
308
+ fields.each do |field|
309
+ if field.is_a? Fieldset
310
+ refresh_lazy_refs(field.fields)
311
+ else
312
+ field.inputs.resolve_lazy_special_args(@special_args)
313
+ refresh_lazy_refs(field.fieldset.fields)
314
+ end
315
+ end
316
+ end
317
+
236
318
  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])
319
+ if token = expect(:EXPRESSION)
320
+ value = parse_expression(token[0][1], token[0][2])
321
+
322
+ # validate fragment refs
323
+ @refs_to_validate&.each do |ref|
324
+ raise "Argument reference #{ref} doesn't exist" unless @special_args[ref]
325
+ end
326
+
327
+ refresh_lazy_refs(value.builder.>>)
328
+
329
+ value
330
+ elsif expect(:FRAGMENT)
331
+ skip_fragments
332
+ parse
241
333
  else
242
- raise "INVALID"
334
+ raise "Expression or Fragment not found"
243
335
  end
244
336
  end
245
337
 
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
338
+ def skip_fragments
339
+ skip_fieldset
252
340
  end
253
341
 
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
342
+ def parse_expression(type, name)
343
+ parse_args
344
+ builder = parse_fieldset_into_builder
345
+ need(:END_FIELDSET)
346
+ query = Query.new(name, type.to_sym, builder: builder)
347
+ query
259
348
  end
260
349
  end
261
350
 
351
+ class LexerError < StandardError; end
352
+
262
353
  class Lexer
263
354
  attr_reader :stack, :scanner
264
-
265
355
  def initialize(gql, scanner: StringScanner.new(gql))
266
356
  @original_string = gql
267
357
  @scanner = scanner
@@ -269,211 +359,283 @@ module Graphlyte
269
359
  end
270
360
 
271
361
  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
362
 
279
363
  def tokenize
280
364
  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
356
- else
357
- handle_field
358
- 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
+ tokenize_objects
366
+ end
367
+
368
+ @tokens
369
+ end
370
+
371
+ def tokenize_objects
372
+ case state
373
+ when :default # the stack is empty, can only process top level fragments or expressions
374
+ if scanner.scan %r{\s*fragment\s*(\w+)\s*on\s*(\w+)}
375
+ @tokens << [:FRAGMENT, scanner[1], scanner[2]]
376
+ push_context :fragments
377
+ # check for a fieldset
378
+ if scanner.check %r[\s*{]
379
+ tokenize_fieldset
372
380
  else
373
- handle_shared_arguments
381
+ scanner.scan /\(/
382
+ @tokens << [:START_ARGS]
383
+ push_state :arguments
374
384
  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
385
+ elsif scanner.check /\{/
386
+ @tokens << [:EXPRESSION, 'query', nil] if get_context == :default
387
+ push_context :fieldset
388
+
389
+ tokenize_fieldset
390
+ elsif scanner.scan %r{^(\w+) (\w+)?}
391
+ @tokens << [:EXPRESSION, scanner[1], scanner[2]]
392
+ push_context :expression
393
+ # check for a fieldset
394
+ if scanner.check %r[\s*{]
395
+ tokenize_fieldset
380
396
  else
381
- push_state :argument_defaults
382
- handle_shared_arguments
397
+ scanner.scan /\(/
398
+ @tokens << [:START_ARGS]
399
+ push_state :arguments
383
400
  end
384
- when :special_args
385
- handle_special_args
401
+ elsif scanner.check /\s*\}/
402
+ end_fieldset
403
+ else
404
+ advance
386
405
  end
406
+ when :fieldset
407
+ tokenize_fields
408
+ when :arguments
409
+ tokenize_arguments
410
+ when :argument_defaults
411
+ tokenize_argument_defaults
412
+ when :hash_arguments
413
+ tokenize_hash_arguments
414
+ when :array_arguments
415
+ tokenize_array_arguments
416
+ when :special_args
417
+ tokenize_special_arguments
418
+ when :inline_fragment
419
+ tokenize_inline_fragment
387
420
  end
388
- @tokens
389
421
  end
390
422
 
391
- private
423
+ def check_for_last(regex = /\s*\}/)
424
+ scanner.check regex
425
+ end
392
426
 
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
427
+ def check_for_final
428
+ scanner.check /\s*\}(?!\s*\})/
429
+ end
430
+
431
+ def check_for_not_last
432
+ scanner.check /\s*\}(?=\s*\})/
433
+ end
434
+
435
+ def tokenize_inline_fragment
436
+ if scanner.scan /on (\w+)/
437
+ @tokens << [:MODEL_NAME, scanner[1]]
438
+
439
+ pop_state
440
+ elsif scanner.scan /@(\w+)/
441
+ @tokens << [:DIRECTIVE, scanner[1]]
442
+
443
+ pop_state
419
444
  else
445
+ # throw an error here?
420
446
  advance
421
447
  end
422
448
  end
423
449
 
424
- def handle_shared_arguments
450
+ def end_fieldset
451
+ scanner.scan /\s*\}/
452
+ @tokens << [:END_FIELDSET]
453
+ pop_state
454
+ pop_context if state == :default
455
+ end
456
+
457
+ def end_arguments
458
+ scanner.scan /\s*\)/
459
+ @tokens << [:END_ARGS]
460
+ pop_state if state == :argument_defaults
461
+ pop_state
462
+ end_fieldset while check_for_last && state == :fieldset
463
+ end
464
+
465
+ # to tired to figure out why this is right now
466
+ def tokenize_argument_defaults
467
+ if scanner.check /\)/
468
+ @tokens << [:END_DEFAULT_VALUE]
469
+ pop_state
470
+ elsif scanner.scan /[\n|,]/
471
+ @tokens << [:END_DEFAULT_VALUE]
472
+ pop_state
473
+ else
474
+ tokenize_shared_arguments
475
+ end
476
+ end
477
+
478
+ def tokenize_special_arguments
479
+ if scanner.check SPECIAL_ARG_REGEX
480
+ scanner.scan SPECIAL_ARG_REGEX
481
+
482
+ @tokens << [:SPECIAL_ARG_VAL, scanner[1]]
483
+
484
+ pop_state
485
+
486
+ end_arguments if check_for_last(/\s*\)/)
487
+ else
488
+ # revisit this.. should we throw an error here?
489
+ pop_state
490
+ raise LexerError, "why can't we parse #{scanner.peek(5)}"
491
+ end
492
+ end
493
+
494
+ def tokenize_array_arguments
495
+ if scanner.scan /\]/
496
+ @tokens << [:ARG_ARRAY_END]
497
+
498
+ pop_state
499
+ # if check_for_last(')')
500
+ # pop_state
501
+ # end
502
+ else
503
+ tokenize_shared_arguments
504
+ end
505
+ end
506
+
507
+ def tokenize_hash_arguments
508
+ if scanner.scan /\}/
509
+ @tokens << [:ARG_HASH_END]
510
+
511
+ pop_state
512
+ # if this is the last argument in the list, maybe get back to the field scope?
513
+ # if check_for_last(')')
514
+ # pop_state
515
+ # end
516
+ else
517
+ tokenize_shared_arguments
518
+ end
519
+ end
520
+
521
+ def pop_argument_state
522
+ if check_for_last(/\s*\)/)
523
+ @tokens << [:END_DEFAULT_VALUE] if state == :argument_defaults
524
+ end_arguments
525
+ else
526
+ pop_state unless %i[arguments argument_defaults hash_arguments array_arguments special_args].include?(state)
527
+ end
528
+ end
529
+
530
+ def tokenize_arguments
531
+ # pop argument state if arguments are finished
532
+ if scanner.scan %r{\)}
533
+ @tokens << [:END_ARGS]
534
+
535
+ pop_state
536
+ # something(argument: $argument = true)
537
+ elsif scanner.scan %r{=}
538
+ @tokens << [:DEFAULT_VALUE]
539
+
540
+ push_state :argument_defaults
541
+ # noop, should expect this, but not important
542
+ elsif scanner.scan %r{,}
543
+ nil
544
+ else
545
+ tokenize_shared_arguments
546
+ end
547
+ end
548
+
549
+ def tokenize_shared_arguments
425
550
  if scanner.scan /^(\w+):/
426
551
  @tokens << [:ARG_KEY, scanner[1]]
427
- elsif scanner.scan /^\s*\{\s*?/
428
- @tokens << [:ARG_HASH_START]
552
+ elsif scanner.scan %r[{]
553
+ @tokens << [:ARG_HASH]
554
+
429
555
  push_state :hash_arguments
430
- elsif scanner.scan /\s*\[\s*/
431
- @tokens << [:ARG_ARRAY_START]
556
+ elsif scanner.scan /\[/
557
+ @tokens << [:ARG_ARRAY]
558
+
432
559
  push_state :array_arguments
433
- elsif scanner.scan /\s?\"([\w\s]+)\"/
560
+ elsif scanner.scan %r{"(.*?)"}
434
561
  @tokens << [:ARG_STRING_VALUE, scanner[1]]
435
- elsif scanner.scan /\s?(\d+\.\d+)/
562
+
563
+ pop_argument_state
564
+ elsif scanner.scan /(\d+\.\d+)/
436
565
  @tokens << [:ARG_FLOAT_VALUE, scanner[1].to_f]
437
- elsif scanner.scan /\s?(\d+)/
566
+
567
+ pop_argument_state
568
+ elsif scanner.scan /(\d+)/
438
569
  @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]
570
+
571
+ pop_argument_state
572
+ elsif scanner.scan /(true|false)/
573
+ @tokens << [:ARG_BOOL_VALUE, (scanner[1] == 'true')]
574
+
575
+ pop_argument_state
442
576
  elsif scanner.scan /\$(\w+):/
443
577
  @tokens << [:SPECIAL_ARG_KEY, scanner[1]]
578
+
444
579
  push_state :special_args
445
580
  elsif scanner.scan /\$(\w+)/
446
581
  @tokens << [:SPECIAL_ARG_REF, scanner[1]]
582
+
583
+ pop_argument_state
584
+ elsif scanner.scan /,/
585
+ # no-op
586
+ elsif scanner.scan /([A-Za-z_"]+)/
587
+ @tokens << [:ARG_LITERAL_VALUE, scanner[1]]
588
+
589
+ elsif check_for_last(/\s*\)/)
590
+ @tokens << [:END_DEFAULT_VALUE] if state == :argument_defaults
591
+ end_arguments
447
592
  else
448
593
  advance
449
594
  end
450
595
  end
451
596
 
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]]
456
- pop_state
457
- else
458
- pop_state
459
- end
460
- end
597
+ def tokenize_fields
598
+ if scanner.check %r[\s*{]
599
+ tokenize_fieldset
600
+ # ... on Model - or - ... @directive
601
+ elsif scanner.scan %r{\.{3}\s}
602
+ @tokens << [:INLINE_FRAGMENT]
603
+ push_state :inline_fragment
604
+ # @directive
605
+ elsif scanner.scan %r{@(\w+)}
606
+ @tokens << [:DIRECTIVE, scanner[1]]
461
607
 
462
- def handle_hash_arguments
463
- if scanner.scan /\}/
464
- @tokens << [:ARG_HASH_END]
465
- pop_state
608
+ end_fieldset while check_for_last && state == :fieldset
609
+ # ...fragmentReference (check for last since it is a field literal)
610
+ elsif scanner.scan /\.{3}(\w+)/
611
+ @tokens << [:FRAGMENT_REF, scanner[1]]
612
+
613
+ end_fieldset while check_for_last && state == :fieldset
614
+ # alias:
615
+ elsif scanner.scan %r{(\w+):}
616
+ @tokens << [:ALIAS, scanner[1]]
617
+ # fieldLiteral
618
+ elsif scanner.scan %r{(\w+)}
619
+ @tokens << [:FIELD_NAME, scanner[1]]
620
+
621
+ end_fieldset while check_for_last && state == :fieldset
622
+ # (arguments: true)
623
+ elsif scanner.scan /^\s*\(/
624
+ @tokens << [:START_ARGS]
625
+
626
+ push_state :arguments
466
627
  else
467
- handle_shared_arguments
628
+ advance
468
629
  end
469
630
  end
470
631
 
471
- def handle_array_arguments
472
- if scanner.scan /\s*\]\s*/
473
- @tokens << [:ARG_ARRAY_END]
474
- pop_state
632
+ def tokenize_fieldset
633
+ if scanner.scan %r[\s*{]
634
+ @tokens << [:FIELDSET]
635
+
636
+ push_state :fieldset
475
637
  else
476
- handle_shared_arguments
638
+ raise LexerError, "Expecting `{` got `#{scanner.peek(3)}`"
477
639
  end
478
640
  end
479
641
 
@@ -498,6 +660,10 @@ module Graphlyte
498
660
  end
499
661
 
500
662
  def advance
663
+ unless scanner.check /\s/
664
+ raise LexerError, "Unexpected Char: '#{scanner.peek(20)}'"
665
+ end
666
+
501
667
  scanner.pos = scanner.pos + 1
502
668
  end
503
669
 
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.2
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-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -47,10 +47,13 @@ files:
47
47
  - lib/graphlyte.rb
48
48
  - lib/graphlyte/arguments/set.rb
49
49
  - lib/graphlyte/arguments/value.rb
50
+ - lib/graphlyte/arguments/value_literal.rb
50
51
  - lib/graphlyte/builder.rb
52
+ - lib/graphlyte/directive.rb
51
53
  - lib/graphlyte/field.rb
52
54
  - lib/graphlyte/fieldset.rb
53
55
  - lib/graphlyte/fragment.rb
56
+ - lib/graphlyte/inline_fragment.rb
54
57
  - lib/graphlyte/query.rb
55
58
  - lib/graphlyte/refinements/string_refinement.rb
56
59
  - lib/graphlyte/schema/parser.rb