graphlyte 0.3.0 → 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.
- checksums.yaml +4 -4
- data/lib/graphlyte/data.rb +68 -0
- data/lib/graphlyte/document.rb +131 -0
- data/lib/graphlyte/dsl.rb +86 -0
- data/lib/graphlyte/editor.rb +288 -0
- data/lib/graphlyte/editors/annotate_types.rb +75 -0
- data/lib/graphlyte/editors/canonicalize.rb +26 -0
- data/lib/graphlyte/editors/collect_variable_references.rb +36 -0
- data/lib/graphlyte/editors/infer_signature.rb +36 -0
- data/lib/graphlyte/editors/inline_fragments.rb +37 -0
- data/lib/graphlyte/editors/remove_unneeded_spreads.rb +64 -0
- data/lib/graphlyte/editors/select_operation.rb +116 -0
- data/lib/graphlyte/editors/with_variables.rb +106 -0
- data/lib/graphlyte/errors.rb +33 -0
- data/lib/graphlyte/lexer.rb +392 -0
- data/lib/graphlyte/lexing/location.rb +43 -0
- data/lib/graphlyte/lexing/token.rb +31 -0
- data/lib/graphlyte/parser.rb +269 -0
- data/lib/graphlyte/parsing/backtracking_parser.rb +160 -0
- data/lib/graphlyte/refinements/string_refinement.rb +14 -8
- data/lib/graphlyte/refinements/syntax_refinements.rb +62 -0
- data/lib/graphlyte/schema.rb +165 -0
- data/lib/graphlyte/schema_query.rb +82 -65
- data/lib/graphlyte/selection_builder.rb +189 -0
- data/lib/graphlyte/selector.rb +75 -0
- data/lib/graphlyte/serializer.rb +223 -0
- data/lib/graphlyte/syntax.rb +369 -0
- data/lib/graphlyte.rb +24 -42
- metadata +88 -18
- data/lib/graphlyte/arguments/set.rb +0 -88
- data/lib/graphlyte/arguments/value.rb +0 -32
- data/lib/graphlyte/builder.rb +0 -53
- data/lib/graphlyte/directive.rb +0 -21
- data/lib/graphlyte/field.rb +0 -65
- data/lib/graphlyte/fieldset.rb +0 -36
- data/lib/graphlyte/fragment.rb +0 -17
- data/lib/graphlyte/inline_fragment.rb +0 -29
- data/lib/graphlyte/query.rb +0 -148
- data/lib/graphlyte/schema/parser.rb +0 -674
- data/lib/graphlyte/schema/types/base.rb +0 -54
- 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
|