graphlyte 0.3.2 → 1.0.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.
Files changed (42) 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 -19
  30. data/lib/graphlyte/arguments/set.rb +0 -94
  31. data/lib/graphlyte/arguments/value.rb +0 -42
  32. data/lib/graphlyte/arguments/value_literal.rb +0 -17
  33. data/lib/graphlyte/builder.rb +0 -59
  34. data/lib/graphlyte/directive.rb +0 -25
  35. data/lib/graphlyte/field.rb +0 -65
  36. data/lib/graphlyte/fieldset.rb +0 -36
  37. data/lib/graphlyte/fragment.rb +0 -17
  38. data/lib/graphlyte/inline_fragment.rb +0 -29
  39. data/lib/graphlyte/query.rb +0 -150
  40. data/lib/graphlyte/schema/parser.rb +0 -687
  41. data/lib/graphlyte/schema/types/base.rb +0 -54
  42. 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