graphlyte 0.2.2 → 0.3.0

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