graphlyte 0.2.3 → 0.3.1

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