graphlyte 0.2.4 → 0.3.2

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