graphlyte 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphlyte/data.rb +68 -0
  3. data/lib/graphlyte/document.rb +131 -0
  4. data/lib/graphlyte/dsl.rb +86 -0
  5. data/lib/graphlyte/editor.rb +288 -0
  6. data/lib/graphlyte/editors/annotate_types.rb +75 -0
  7. data/lib/graphlyte/editors/canonicalize.rb +26 -0
  8. data/lib/graphlyte/editors/collect_variable_references.rb +36 -0
  9. data/lib/graphlyte/editors/infer_signature.rb +36 -0
  10. data/lib/graphlyte/editors/inline_fragments.rb +37 -0
  11. data/lib/graphlyte/editors/remove_unneeded_spreads.rb +64 -0
  12. data/lib/graphlyte/editors/select_operation.rb +116 -0
  13. data/lib/graphlyte/editors/with_variables.rb +106 -0
  14. data/lib/graphlyte/errors.rb +33 -0
  15. data/lib/graphlyte/lexer.rb +392 -0
  16. data/lib/graphlyte/lexing/location.rb +43 -0
  17. data/lib/graphlyte/lexing/token.rb +31 -0
  18. data/lib/graphlyte/parser.rb +269 -0
  19. data/lib/graphlyte/parsing/backtracking_parser.rb +160 -0
  20. data/lib/graphlyte/refinements/string_refinement.rb +14 -8
  21. data/lib/graphlyte/refinements/syntax_refinements.rb +62 -0
  22. data/lib/graphlyte/schema.rb +165 -0
  23. data/lib/graphlyte/schema_query.rb +82 -65
  24. data/lib/graphlyte/selection_builder.rb +189 -0
  25. data/lib/graphlyte/selector.rb +75 -0
  26. data/lib/graphlyte/serializer.rb +223 -0
  27. data/lib/graphlyte/syntax.rb +369 -0
  28. data/lib/graphlyte.rb +24 -42
  29. metadata +88 -18
  30. data/lib/graphlyte/arguments/set.rb +0 -88
  31. data/lib/graphlyte/arguments/value.rb +0 -32
  32. data/lib/graphlyte/builder.rb +0 -53
  33. data/lib/graphlyte/directive.rb +0 -21
  34. data/lib/graphlyte/field.rb +0 -65
  35. data/lib/graphlyte/fieldset.rb +0 -36
  36. data/lib/graphlyte/fragment.rb +0 -17
  37. data/lib/graphlyte/inline_fragment.rb +0 -29
  38. data/lib/graphlyte/query.rb +0 -148
  39. data/lib/graphlyte/schema/parser.rb +0 -674
  40. data/lib/graphlyte/schema/types/base.rb +0 -54
  41. data/lib/graphlyte/types.rb +0 -9
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './syntax'
4
+ require_relative './refinements/string_refinement'
5
+
6
+ module Graphlyte
7
+ # Helper to build arguments for a field selection.
8
+ class ArgumentBuilder
9
+ using Graphlyte::Refinements::StringRefinement
10
+
11
+ def initialize(document)
12
+ @document = document
13
+ end
14
+
15
+ def build(arguments)
16
+ return [] unless arguments.any?
17
+
18
+ arguments.to_a.map do |(k, v)|
19
+ value = case v
20
+ when Syntax::Value
21
+ v # built via Graphlyte.enum for example
22
+ when SelectionBuilder::Variable
23
+ @document.declare(v)
24
+ when Symbol
25
+ Syntax::VariableReference.new(v.name.camelize)
26
+ else
27
+ Syntax::Value.from_ruby(v)
28
+ end
29
+
30
+ Syntax::Argument.new(k.to_s.camelize, value)
31
+ end
32
+ end
33
+ end
34
+
35
+ # The return value from `select!`. Allows further modifications (aliasing,
36
+ # directives) to the field.
37
+ class WithField
38
+ def initialize(field, builder)
39
+ @field = field
40
+ @builder = builder
41
+ end
42
+
43
+ def alias(name, &block)
44
+ @field.as = name
45
+
46
+ @field.selection += @builder.build!(&block) if block_given?
47
+
48
+ self
49
+ end
50
+
51
+ def method_missing(name, *_args, **kwargs, &block)
52
+ directive = Syntax::Directive.new(name.to_s)
53
+
54
+ directive.arguments = @builder.argument_builder!.build(kwargs) if kwargs.any?
55
+
56
+ @field.selection += @builder.build!(&block) if block_given?
57
+
58
+ @field.directives << directive
59
+
60
+ self
61
+ end
62
+
63
+ def respond_to_missing?(*)
64
+ true
65
+ end
66
+ end
67
+
68
+ # Main construct used to build selection sets, uses `method_missing` to
69
+ # select fields.
70
+ #
71
+ # Note: instance methods are either symbolic or end in bangs to avoid
72
+ # shadowing legal field names.
73
+ #
74
+ # Usage:
75
+ #
76
+ # some_fields = %w[all these fields]
77
+ # selection = SelectionBuilder.build(document) do
78
+ # foo # basic field
79
+ # bar(baz: 1) { x; y; z} # field with sub-selection
80
+ # some_fields.each { self << _1 } # Adding fields dynamically
81
+ # end
82
+ #
83
+ # You should probably never need to call this directly - it is used to
84
+ # implement the DSL class.
85
+ class SelectionBuilder
86
+ using Graphlyte::Refinements::StringRefinement
87
+
88
+ # Variables should not be re-used between queries
89
+ Variable = Struct.new(:type, :name, keyword_init: true)
90
+
91
+ def self.build(document, &block)
92
+ return [] unless block_given?
93
+
94
+ new(document).build!(&block)
95
+ end
96
+
97
+ def initialize(document)
98
+ @document = document
99
+ end
100
+
101
+ def build!
102
+ old = @selection
103
+ curr = []
104
+ return curr unless block_given?
105
+
106
+ @selection = curr
107
+
108
+ yield self
109
+
110
+ curr
111
+ ensure
112
+ @selection = old
113
+ end
114
+
115
+ def on!(type_name, &block)
116
+ frag = Graphlyte::Syntax::InlineFragment.new
117
+ frag.type_name = type_name
118
+ frag.selection = build!(&block)
119
+
120
+ select! frag
121
+ end
122
+
123
+ # Selected can be:
124
+ #
125
+ # - a string or symbol (field name)
126
+ # - a Graphlyte::Syntax::{Fragment,Field,InlineFragment}
127
+ # - a SelectionBuilder::Variable (constructed with `DSL#var`).
128
+ #
129
+ # Use of this method (or `select!`) is necessary to add fields
130
+ # that shadow core method or construct names (e.g. `if`, `open`, `else`,
131
+ # `class` and so on).
132
+ def <<(selected)
133
+ select!(selected)
134
+ end
135
+
136
+ def select!(selected, *args, **kwargs, &block)
137
+ case selected
138
+ when Graphlyte::Syntax::Fragment
139
+ @document.add_fragments(selected.required_fragments)
140
+ @selection << Graphlyte::Syntax::FragmentSpread.new(name: selected.name)
141
+ when Graphlyte::Syntax::InlineFragment, Graphlyte::Syntax::Field
142
+ @selection << selected
143
+ else
144
+ field = new_field!(selected.to_s, args)
145
+ field.arguments = argument_builder!.build(kwargs)
146
+ field.selection += self.class.build(@document, &block)
147
+
148
+ WithField.new(field, self)
149
+ end
150
+ end
151
+
152
+ def argument_builder!
153
+ @argument_builder ||= ArgumentBuilder.new(@document)
154
+ end
155
+
156
+ private
157
+
158
+ def new_field!(name, args)
159
+ field = Syntax::Field.new(name: name)
160
+ @selection << field
161
+
162
+ args.each do |arg|
163
+ case arg
164
+ when Symbol
165
+ field.directives << Syntax::Directive.new(arg.to_s)
166
+ when WithField
167
+ raise ArgumentError, 'Reference error' # caused by typos usually.
168
+ else
169
+ field.selection += self.class.build(@document) { _1.select! arg }
170
+ end
171
+ end
172
+
173
+ field
174
+ end
175
+
176
+ def method_missing(name, *args, **kwargs, &block)
177
+ if name.to_s.end_with?('=') && args.length == 1 && args[0].is_a?(WithField)
178
+ aka = name.to_s.chomp('=')
179
+ args[0].alias(aka)
180
+ else
181
+ select!(name.to_s.camelize, *args, **kwargs.transform_keys(&:camelize), &block)
182
+ end
183
+ end
184
+
185
+ def respond_to_missing?(*)
186
+ true
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './syntax'
4
+ require_relative './editor'
5
+
6
+ module Graphlyte
7
+ # A tool for simple editing of GraphQL queries using a path-based API
8
+ #
9
+ # Usage:
10
+ #
11
+ # editor = Selector.new
12
+ # editor.at('project.pipelines.nodes.status', &:remove)
13
+ # editor.at('project.pipelines.nodes') do |node|
14
+ # node.append do
15
+ # downstream do
16
+ # nodes { active }
17
+ # end
18
+ # end
19
+ #
20
+ # editor.edit(doc)
21
+ #
22
+ class Selector
23
+ def initialize
24
+ @actions = {}
25
+ end
26
+
27
+ def at(path, &block)
28
+ raise ArgumentError 'block not given' unless block_given?
29
+
30
+ @actions[path] = block
31
+
32
+ self
33
+ end
34
+
35
+ def edit(doc)
36
+ editor = Editor.new.on_field do |field, action|
37
+ edit_field(field, action)
38
+ end
39
+
40
+ editor.edit(doc)
41
+
42
+ doc
43
+ end
44
+
45
+ def edit_field(field, action)
46
+ key = action.path.map { _1.name if _1.is_a?(Syntax::Field) }.compact.join('.')
47
+ block = @actions[key]
48
+
49
+ block&.call(SelectAction.new(field, action))
50
+ end
51
+
52
+ # Each block defined with `at` receives as its only argument a
53
+ # `SelectAction`. This object exposes method allowing the caller
54
+ # to modify the query.
55
+ class SelectAction
56
+ def initialize(field, action)
57
+ @field = field
58
+ @action = action
59
+ end
60
+
61
+ # Remove the current node.
62
+ def remove
63
+ @action.delete
64
+ end
65
+
66
+ # Construct a new selection using the block, and append it to the
67
+ # current field selection.
68
+ def append(&block)
69
+ selection = SelectionBuilder.build(@action.document, &block)
70
+
71
+ @field.selection += selection
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './syntax'
4
+
5
+ module Graphlyte
6
+ # Logic for writing a GraphQL document to a string
7
+ class Serializer
8
+ # Type directed serialization:
9
+ module Refinements
10
+ refine Syntax::VariableReference do
11
+ def serialize(buff)
12
+ buff << "$#{variable}"
13
+ end
14
+ end
15
+
16
+ refine Syntax::Argument do
17
+ def serialize(buff)
18
+ buff << "#{name}: "
19
+ value.serialize(buff)
20
+ end
21
+ end
22
+
23
+ refine Syntax::VariableDefinition do
24
+ def serialize(buff)
25
+ buff << "$#{variable}: #{type}"
26
+ if default_value
27
+ buff << ' = '
28
+ default_value.serialize(buff)
29
+ end
30
+ buff.dump_directives(self)
31
+ end
32
+ end
33
+
34
+ refine Syntax::Directive do
35
+ def serialize(buff)
36
+ buff << ' @' << name
37
+ buff.dump_arguments(self)
38
+ end
39
+ end
40
+
41
+ refine Syntax::Field do
42
+ def serialize(buff)
43
+ buff << "#{as}: " if as
44
+ buff << name
45
+ buff.dump_arguments(self)
46
+ buff.dump_directives(self)
47
+ buff.dump_selection(self)
48
+ end
49
+ end
50
+
51
+ refine Syntax::FragmentSpread do
52
+ def serialize(buff)
53
+ buff << '...' << name
54
+ buff.dump_directives(self)
55
+ end
56
+ end
57
+
58
+ refine Syntax::InlineFragment do
59
+ def serialize(buff)
60
+ buff << '... on ' << type_name
61
+ buff.dump_directives(self)
62
+ buff.dump_selection(self)
63
+ end
64
+ end
65
+
66
+ refine Syntax::Operation do
67
+ def serialize(buff)
68
+ buff << type
69
+ buff << " #{name}" if name
70
+ buff.comma_separated(variables)
71
+ buff.dump_directives(self)
72
+ buff.dump_selection(self)
73
+ end
74
+ end
75
+
76
+ refine Syntax::Fragment do
77
+ def serialize(buff)
78
+ buff << 'fragment ' << name << ' on ' << type_name
79
+ buff.dump_directives(self)
80
+ buff.dump_selection(self)
81
+ end
82
+ end
83
+
84
+ refine Syntax::Value do
85
+ def serialize(buff)
86
+ buff << case value
87
+ when String
88
+ # TODO: handle block strings?
89
+ "\"#{value.gsub(/"/, '\"').gsub("\n", '\n').gsub("\t", '\t')}\""
90
+ else
91
+ value
92
+ end
93
+ end
94
+ end
95
+
96
+ refine Array do
97
+ def serialize(buff)
98
+ # TODO: handle layout of large arrays nicely
99
+ buff << '['
100
+ each_with_index do |v, i|
101
+ buff << (', ' * [i, 1].min)
102
+ v.serialize(buff)
103
+ end
104
+ buff << ']'
105
+ end
106
+ end
107
+
108
+ refine Hash do
109
+ def serialize(buff)
110
+ buff << '{'
111
+ each.each_with_index do |(k, v), i|
112
+ buff << (', ' * [i, 1].min)
113
+ buff << k
114
+ buff << ': '
115
+ v.serialize(buff)
116
+ end
117
+ buff << '}'
118
+ end
119
+ end
120
+ end
121
+
122
+ using Refinements
123
+
124
+ Unsupported = Class.new(ArgumentError)
125
+ SPACE = ' '
126
+ STEP = 2
127
+ NEWLINE = "\n"
128
+
129
+ attr_reader :buff, :indent
130
+ attr_accessor :line_length, :max_fields_per_line
131
+
132
+ def initialize(buff = [])
133
+ @buff = buff
134
+ @line_length = 100
135
+ @max_fields_per_line = 5
136
+ @indent = 0
137
+ end
138
+
139
+ def <<(chunk)
140
+ @buff << chunk.to_s
141
+ self
142
+ end
143
+
144
+ def dump_definitions(definitions)
145
+ return unless definitions&.any?
146
+
147
+ definitions.each_with_index do |dfn, i|
148
+ buff << NEWLINE << NEWLINE if i.positive?
149
+ dfn.serialize(self)
150
+ rescue NoMethodError
151
+ raise Unsupported, dfn.class
152
+ end
153
+ end
154
+
155
+ def dump_selection(node)
156
+ i = @indent
157
+ selection = node.selection
158
+ return unless selection&.any?
159
+
160
+ @indent += STEP
161
+
162
+ buff << SPACE << '{'
163
+
164
+ if simple?(selection)
165
+ dump_simple_selection(selection)
166
+ else
167
+ dump_indented_selection(selection)
168
+ end
169
+
170
+ buff << '}'
171
+ ensure
172
+ @indent = i
173
+ end
174
+
175
+ def next_line
176
+ buff << NEWLINE << (SPACE * indent)
177
+ end
178
+
179
+ def dump_indented_selection(selection)
180
+ selection.each do |selected|
181
+ next_line
182
+
183
+ selected.serialize(self)
184
+ end
185
+
186
+ buff << NEWLINE << (SPACE * (indent - STEP))
187
+ end
188
+
189
+ def simple?(selection)
190
+ selection.length < max_fields_per_line &&
191
+ selection.all?(&:simple?) &&
192
+ (selection.sum { _1.name.length } + selection.length + indent) < line_length
193
+ end
194
+
195
+ def dump_simple_selection(selection)
196
+ buff << SPACE
197
+ selection.each do |selected|
198
+ selected.serialize(self)
199
+ buff << SPACE
200
+ end
201
+ end
202
+
203
+ def dump_directives(node)
204
+ node.directives&.each { _1.serialize(self) }
205
+ end
206
+
207
+ def comma_separated(collection)
208
+ return unless collection
209
+ return if collection.empty?
210
+
211
+ buff << '('
212
+ collection.each_with_index do |elem, i|
213
+ buff << (', ' * [i, 1].min)
214
+ elem.serialize(self)
215
+ end
216
+ buff << ')'
217
+ end
218
+
219
+ def dump_arguments(node)
220
+ comma_separated(node.arguments)
221
+ end
222
+ end
223
+ end