graphlyte 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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