graphlyte 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50a30d874f185f08a1d8e9d7911056135927ec25a2f9e272b14dd50124aeea79
|
4
|
+
data.tar.gz: 97b938f8391811a7da0a29d855daea3812c7e97e953dd64fc5fa90fd98a10b8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebe479ecb5e89d21e0e193207cd72fa6891df00621882f0de1d9fa344655d8058ee566857fc12e3d7ea25d34c83f1b6fb6545ea30739e4473695ef875ef6661a
|
7
|
+
data.tar.gz: 7d1bc0754b536f724f4e9ef4f91b3d5411335e568227a4e77e171a45c5a5d719b4edf3c4f289504321f5662d8d25571d7041970fc0bf873e54a489f9698e1d0e
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Graphlyte
|
6
|
+
# Very simplistic data-class. Inheritance is not modelled.
|
7
|
+
class Data
|
8
|
+
def self.attr_accessor(*names)
|
9
|
+
super
|
10
|
+
attributes.merge(names)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.attr_reader(*names)
|
14
|
+
super
|
15
|
+
attributes.merge(names)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.attributes
|
19
|
+
@attributes ||= [].to_set
|
20
|
+
end
|
21
|
+
|
22
|
+
# Permissive constructor: ignores unknown attributes
|
23
|
+
def initialize(**kwargs)
|
24
|
+
self.class.attributes.each do |arg|
|
25
|
+
send(:"#{arg}=", kwargs[arg]) if kwargs.key?(arg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def eql?(other)
|
30
|
+
other.is_a?(self.class) && state == other.send(:state)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
eql?(other)
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash
|
38
|
+
state.hash
|
39
|
+
end
|
40
|
+
|
41
|
+
def dup
|
42
|
+
self.class.new(**self.class.attributes.to_h { [_1, dup_attribute(_1)] })
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect
|
46
|
+
"#<#{self.class} #{self.class.attributes.map { "@#{_1}=#{send(_1).inspect}" }.join(' ')}>"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def dup_attribute(attr)
|
52
|
+
value = send(attr)
|
53
|
+
|
54
|
+
case value
|
55
|
+
when Array
|
56
|
+
value.map(&:dup)
|
57
|
+
when Hash
|
58
|
+
value.transform_values(&:dup)
|
59
|
+
else
|
60
|
+
value.dup
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def state
|
65
|
+
self.class.attributes.map { send _1 }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require_relative './syntax'
|
6
|
+
require_relative './data'
|
7
|
+
require_relative './serializer'
|
8
|
+
require_relative './refinements/string_refinement'
|
9
|
+
require_relative './editors/with_variables'
|
10
|
+
|
11
|
+
module Graphlyte
|
12
|
+
# The representation of a GraphQL document.
|
13
|
+
#
|
14
|
+
# Documents can have multiple definitions, which can
|
15
|
+
# be queries, mutations, subscriptions (operations) or fragments.
|
16
|
+
#
|
17
|
+
# During execution, only one operation can be executed.
|
18
|
+
class Document < Graphlyte::Data
|
19
|
+
using Graphlyte::Refinements::StringRefinement
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
attr_accessor :definitions, :variables, :schema
|
23
|
+
|
24
|
+
def_delegators :@definitions, :length, :empty?
|
25
|
+
|
26
|
+
def initialize(**kwargs)
|
27
|
+
super
|
28
|
+
@definitions ||= []
|
29
|
+
@variables ||= {}
|
30
|
+
@var_name_counter = @variables.size + 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def +(other)
|
34
|
+
return dup unless other
|
35
|
+
|
36
|
+
other = other.dup
|
37
|
+
doc = dup
|
38
|
+
|
39
|
+
defs = doc.definitions + other.definitions
|
40
|
+
vars = doc.variables.merge(other.variables) # TODO: detect conflicts?
|
41
|
+
|
42
|
+
self.class.new(definitions: defs, vars: vars)
|
43
|
+
end
|
44
|
+
|
45
|
+
def eql?(other)
|
46
|
+
other.is_a?(self.class) && other.fragments == fragments && other.operations == operations
|
47
|
+
end
|
48
|
+
|
49
|
+
alias == eql?
|
50
|
+
|
51
|
+
def define(dfn)
|
52
|
+
@definitions << dfn
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_fragments(frags)
|
56
|
+
current = fragments
|
57
|
+
|
58
|
+
frags.each do |frag|
|
59
|
+
@definitions << frag unless current[frag.name]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def declare(var)
|
64
|
+
if var.name.nil?
|
65
|
+
var.name = "var#{@var_name_counter}"
|
66
|
+
@var_name_counter += 1
|
67
|
+
end
|
68
|
+
|
69
|
+
parser = Graphlyte::Parser.new(tokens: Graphlyte::Lexer.lex(var.type))
|
70
|
+
parsed_type = parser.type_name! if var.type
|
71
|
+
current_def = @variables[var.name]
|
72
|
+
|
73
|
+
if current_def && current_def.type != parsed_type
|
74
|
+
msg = "Cannot re-declare #{var.name} at different types. #{current_def.type} != #{var.type}"
|
75
|
+
raise ArgumentError, msg
|
76
|
+
end
|
77
|
+
|
78
|
+
@variables[var.name] ||= Graphlyte::Syntax::VariableDefinition.new(
|
79
|
+
variable: var.name,
|
80
|
+
type: parsed_type
|
81
|
+
)
|
82
|
+
|
83
|
+
Syntax::VariableReference.new(var.name, parsed_type)
|
84
|
+
end
|
85
|
+
|
86
|
+
def fragments
|
87
|
+
definitions.select { _1.is_a?(Graphlyte::Syntax::Fragment) }.to_h { [_1.name, _1] }
|
88
|
+
end
|
89
|
+
|
90
|
+
def operations
|
91
|
+
@definitions.select { _1.is_a?(Graphlyte::Syntax::Operation) }.to_h { [_1.name, _1] }
|
92
|
+
end
|
93
|
+
|
94
|
+
def executable?
|
95
|
+
@definitions.all?(&:executable?)
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
buff = []
|
100
|
+
write(buff)
|
101
|
+
|
102
|
+
buff.join
|
103
|
+
end
|
104
|
+
|
105
|
+
# More efficient for writing to files or streams - avoids building up the full string.
|
106
|
+
def write(io)
|
107
|
+
Graphlyte::Serializer.new(io).dump_definitions(definitions)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return this document as a JSON request body, suitable for posting to a server.
|
111
|
+
def request_body(operation = nil, **variables)
|
112
|
+
if operation.nil? && operations.size != 1
|
113
|
+
raise ArgumentError, 'Operation name is required when the document contains multiple operations'
|
114
|
+
end
|
115
|
+
|
116
|
+
variables.transform_keys! { _1.to_s.camelize }
|
117
|
+
|
118
|
+
doc = Editors::WithVariables.new(schema, operation, variables).edit(dup)
|
119
|
+
|
120
|
+
{
|
121
|
+
query: doc.to_s,
|
122
|
+
variables: variables,
|
123
|
+
operation: operation
|
124
|
+
}.compact.to_json
|
125
|
+
end
|
126
|
+
|
127
|
+
def variable_references
|
128
|
+
Editors::CollectVariableReferences.new.edit(self)[Syntax::Operation]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './syntax'
|
4
|
+
require_relative './selection_builder'
|
5
|
+
require_relative './refinements/string_refinement'
|
6
|
+
require_relative './editors/infer_signature'
|
7
|
+
|
8
|
+
module Graphlyte
|
9
|
+
# The DSL methods for query construction are defined here.
|
10
|
+
#
|
11
|
+
# The main methods are:
|
12
|
+
#
|
13
|
+
# - `var`: creates a fresh unique variable
|
14
|
+
# - `enum`: allows referring to enum values
|
15
|
+
# - `fragment`: creates a fragment that can be re-used in operations
|
16
|
+
# - `query`: creates a `Query` operation
|
17
|
+
# - `mutation`: creates a `Mutation` operation
|
18
|
+
class DSL
|
19
|
+
using Graphlyte::Refinements::StringRefinement
|
20
|
+
|
21
|
+
attr_reader :schema
|
22
|
+
|
23
|
+
def initialize(schema = nil)
|
24
|
+
@schema = schema
|
25
|
+
end
|
26
|
+
|
27
|
+
def var(type = nil, name = nil)
|
28
|
+
SelectionBuilder::Variable.new(type: type, name: name&.to_s&.camelize)
|
29
|
+
end
|
30
|
+
|
31
|
+
def enum(value)
|
32
|
+
Syntax::Value.new(value.to_sym, :ENUM)
|
33
|
+
end
|
34
|
+
|
35
|
+
def query(name = nil, doc = Document.new, &block)
|
36
|
+
op = Syntax::Operation.new(type: :query)
|
37
|
+
doc.define(op)
|
38
|
+
|
39
|
+
op.name = name
|
40
|
+
op.selection = SelectionBuilder.build(doc, &block)
|
41
|
+
|
42
|
+
Editors::InferSignature.new(@schema).edit(doc)
|
43
|
+
|
44
|
+
doc
|
45
|
+
end
|
46
|
+
|
47
|
+
def mutation(name = nil, doc = Document.new, &block)
|
48
|
+
op = Syntax::Operation.new(type: :mutation)
|
49
|
+
doc.define(op)
|
50
|
+
|
51
|
+
op.name = name
|
52
|
+
op.selection = SelectionBuilder.build(doc, &block)
|
53
|
+
|
54
|
+
# TODO: infer operation signatures (requires schema!)
|
55
|
+
doc
|
56
|
+
end
|
57
|
+
|
58
|
+
def fragment(fragment_name = nil, on:, doc: Document.new, &block)
|
59
|
+
frag = Graphlyte::Syntax::Fragment.new
|
60
|
+
|
61
|
+
frag.type_name = on
|
62
|
+
frag.selection = SelectionBuilder.build(doc, &block)
|
63
|
+
|
64
|
+
if fragment_name
|
65
|
+
frag.name = fragment_name
|
66
|
+
else
|
67
|
+
base = "#{on}Fields"
|
68
|
+
n = 1
|
69
|
+
frag.name = base
|
70
|
+
|
71
|
+
while doc.fragments[frag.name]
|
72
|
+
frag.name = "#{base}_#{n}"
|
73
|
+
n += 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
doc.fragments.each_value do |required|
|
78
|
+
frag.refers_to required
|
79
|
+
end
|
80
|
+
|
81
|
+
doc.define(frag)
|
82
|
+
|
83
|
+
frag
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './syntax'
|
4
|
+
|
5
|
+
module Graphlyte
|
6
|
+
# Walk the document tree and edit or collect data.
|
7
|
+
#
|
8
|
+
# This is the general purpose recursive transformer for
|
9
|
+
# syntax trees, used to write various validation and
|
10
|
+
# transformation passes. See `lib/graphlyte/editors`
|
11
|
+
#
|
12
|
+
# Usage
|
13
|
+
#
|
14
|
+
# A fragment inliner:
|
15
|
+
#
|
16
|
+
# inliner = Editor.new.on_fragment_spread do |spread, action|
|
17
|
+
# action.replace action.document.fragments[spread.name].inline
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# inliner.edit(document)
|
21
|
+
#
|
22
|
+
# A variable renamer:
|
23
|
+
#
|
24
|
+
# renamer = Editor.new.on_variable do |var|
|
25
|
+
# var.variable = 'x' if var.variable == 'y'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# renamer.edit(document)
|
29
|
+
#
|
30
|
+
# A string collector:
|
31
|
+
#
|
32
|
+
# strings = []
|
33
|
+
# collector = Editor.new.on_value do |value|
|
34
|
+
# strings << value.value if value.type == :STRING
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# collector.edit(document)
|
38
|
+
#
|
39
|
+
class Editor
|
40
|
+
Deleted = Class.new(StandardError)
|
41
|
+
|
42
|
+
attr_accessor :direction
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@hooks = {}
|
46
|
+
@direction = :bottom_up
|
47
|
+
end
|
48
|
+
|
49
|
+
# The value passed to the handler blocks, in addition to the syntax node.
|
50
|
+
# Users can call methods on this object to edit the document in-place, as
|
51
|
+
# well as read information about the context of this node.
|
52
|
+
class Action
|
53
|
+
attr_reader :new_nodes, :path, :definition, :parent, :document
|
54
|
+
|
55
|
+
def initialize(old_node, path, parent, document)
|
56
|
+
@new_nodes = [old_node]
|
57
|
+
@definition = path.first
|
58
|
+
@path = path.dup.freeze
|
59
|
+
@parent = parent
|
60
|
+
@document = document
|
61
|
+
end
|
62
|
+
|
63
|
+
def replace(replacement)
|
64
|
+
@new_nodes = [replacement]
|
65
|
+
end
|
66
|
+
|
67
|
+
def insert_before(node)
|
68
|
+
@new_nodes = [node] + @new_nodes
|
69
|
+
end
|
70
|
+
|
71
|
+
def insert_after(node)
|
72
|
+
@new_nodes.push(node)
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete
|
76
|
+
@new_nodes = []
|
77
|
+
end
|
78
|
+
|
79
|
+
def expand(new_nodes)
|
80
|
+
@new_nodes = new_nodes
|
81
|
+
end
|
82
|
+
|
83
|
+
def closest(node_type)
|
84
|
+
@path.reverse.find { _1.is_a?(node_type) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# The class responsible for orchestration of the hooks. This class
|
89
|
+
# defines the recursion through the document.
|
90
|
+
Context = Struct.new(:document, :direction, :hooks, :path) do
|
91
|
+
def edit(object, &block)
|
92
|
+
parent = path.last
|
93
|
+
path.push(object)
|
94
|
+
|
95
|
+
processor = hooks[object.class]
|
96
|
+
action = Action.new(object, path, parent, document)
|
97
|
+
|
98
|
+
case direction
|
99
|
+
when :bottom_up
|
100
|
+
edit_bottom_up(object, processor, action, &block)
|
101
|
+
when :top_down
|
102
|
+
edit_top_down(object, processor, action, &block)
|
103
|
+
else
|
104
|
+
raise ArgumentError, "Unknown direction: #{direction}"
|
105
|
+
end
|
106
|
+
|
107
|
+
action.new_nodes
|
108
|
+
ensure
|
109
|
+
path.pop
|
110
|
+
end
|
111
|
+
|
112
|
+
def edit_top_down(object, processor, action)
|
113
|
+
processor&.call(object, action)
|
114
|
+
action.new_nodes = action.new_nodes.filter_map do |node|
|
115
|
+
yield node if block_given?
|
116
|
+
node
|
117
|
+
rescue Deleted
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def edit_bottom_up(object, processor, action)
|
123
|
+
yield object if block_given?
|
124
|
+
processor&.call(object, action)
|
125
|
+
rescue Deleted
|
126
|
+
action.new_nodes = []
|
127
|
+
end
|
128
|
+
|
129
|
+
def edit_variables(object)
|
130
|
+
return unless object.respond_to?(:variables)
|
131
|
+
|
132
|
+
object.variables = object.variables&.flat_map do |var|
|
133
|
+
edit(var) do |v|
|
134
|
+
edit_directives(v)
|
135
|
+
v.default_value = edit_value(v.default_value).first
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def edit_directives(object)
|
141
|
+
return unless object.respond_to?(:directives)
|
142
|
+
|
143
|
+
object.directives = object.directives&.flat_map do |dir|
|
144
|
+
edit(dir) { |d| edit_arguments(d) }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def edit_arguments(object)
|
149
|
+
return unless object.respond_to?(:arguments)
|
150
|
+
|
151
|
+
object.arguments = object.arguments&.flat_map do |arg|
|
152
|
+
edit(arg) do |_a|
|
153
|
+
arg.value = edit_value(arg.value).first
|
154
|
+
raise Deleted if arg.value.nil?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def edit_value(object)
|
160
|
+
case object
|
161
|
+
when Array
|
162
|
+
[object.flat_map { edit_value(_1) }]
|
163
|
+
when Hash
|
164
|
+
[
|
165
|
+
object.to_a.flat_map do |(k, old_value)|
|
166
|
+
edit_value(old_value).take(1).map do |new_value|
|
167
|
+
[k, new_value]
|
168
|
+
end
|
169
|
+
end.to_h
|
170
|
+
]
|
171
|
+
else
|
172
|
+
edit(object)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def edit_selection(object)
|
177
|
+
return unless object.respond_to?(:selection)
|
178
|
+
|
179
|
+
object.selection = object.selection&.flat_map do |selected|
|
180
|
+
edit(selected) do |s|
|
181
|
+
edit_arguments(s)
|
182
|
+
edit_directives(s)
|
183
|
+
edit_selection(s)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def edit_definition(object)
|
189
|
+
edit(object) do |o|
|
190
|
+
edit_variables(o)
|
191
|
+
edit_directives(o)
|
192
|
+
edit_selection(o)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.top_down
|
198
|
+
e = new
|
199
|
+
e.direction = :top_down
|
200
|
+
|
201
|
+
e
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.bottom_up
|
205
|
+
new
|
206
|
+
end
|
207
|
+
|
208
|
+
def on_value(&block)
|
209
|
+
@hooks[Syntax::Value] = block
|
210
|
+
self
|
211
|
+
end
|
212
|
+
|
213
|
+
def on_argument(&block)
|
214
|
+
@hooks[Syntax::Argument] = block
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
def on_directive(&block)
|
219
|
+
@hooks[Syntax::Directive] = block
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
def on_operation(&block)
|
224
|
+
@hooks[Syntax::Operation] = block
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
def on_variable(&block)
|
229
|
+
on_variable_definition(&block)
|
230
|
+
on_variable_reference(&block)
|
231
|
+
self
|
232
|
+
end
|
233
|
+
|
234
|
+
def on_variable_definition(&block)
|
235
|
+
@hooks[Syntax::VariableDefinition] = block
|
236
|
+
self
|
237
|
+
end
|
238
|
+
|
239
|
+
def on_variable_reference(&block)
|
240
|
+
@hooks[Syntax::VariableReference] = block
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
# Selected nodes:
|
245
|
+
|
246
|
+
def on_field(&block)
|
247
|
+
@hooks[Syntax::Field] = block
|
248
|
+
self
|
249
|
+
end
|
250
|
+
|
251
|
+
def on_fragment(&block)
|
252
|
+
on_inline_fragment(&block)
|
253
|
+
on_fragment_definition(&block)
|
254
|
+
self
|
255
|
+
end
|
256
|
+
|
257
|
+
def on_fragment_spread(&block)
|
258
|
+
@hooks[Syntax::FragmentSpread] = block
|
259
|
+
self
|
260
|
+
end
|
261
|
+
|
262
|
+
def on_inline_fragment(&block)
|
263
|
+
@hooks[Syntax::InlineFragment] = block
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
def on_fragment_definition(&block)
|
268
|
+
@hooks[Syntax::Fragment] = block
|
269
|
+
self
|
270
|
+
end
|
271
|
+
|
272
|
+
# To edit specific nodes in a document (or isolated from a document)
|
273
|
+
# you will need a Context.
|
274
|
+
def context(document = nil)
|
275
|
+
Context.new(document, direction, @hooks.dup.freeze, [])
|
276
|
+
end
|
277
|
+
|
278
|
+
def edit(document)
|
279
|
+
c = context(document)
|
280
|
+
|
281
|
+
document.definitions = document.definitions.flat_map do |object|
|
282
|
+
c.edit_definition(object)
|
283
|
+
end
|
284
|
+
|
285
|
+
document
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Graphlyte
|
4
|
+
module Editors
|
5
|
+
# Use a schema definition to annotate the type of each field and variable reference.
|
6
|
+
class AnnotateTypes
|
7
|
+
TypeCheckError = Class.new(StandardError)
|
8
|
+
TypeNotFound = Class.new(TypeCheckError)
|
9
|
+
FieldNotFound = Class.new(TypeCheckError)
|
10
|
+
CannotDetermineTypeName = Class.new(TypeCheckError)
|
11
|
+
|
12
|
+
def initialize(schema, recheck: false)
|
13
|
+
@schema = schema
|
14
|
+
@recheck = recheck
|
15
|
+
end
|
16
|
+
|
17
|
+
def edit(doc)
|
18
|
+
return if !recheck && doc.schema # Previously annotated
|
19
|
+
|
20
|
+
doc.schema = @schema
|
21
|
+
editor.edit(doc)
|
22
|
+
end
|
23
|
+
|
24
|
+
def editor
|
25
|
+
@editor ||=
|
26
|
+
Editor
|
27
|
+
.top_down
|
28
|
+
.on_field { |field, action| infer_field(field, action.parent, action.document) }
|
29
|
+
.on_variable_reference do |ref, action|
|
30
|
+
infer_ref(ref, action.closest(Syntax::Argument), action.closest(Syntax::Field))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# For now we are ignoring variables nested in input objects.
|
35
|
+
# TODO: encode input objects differently?
|
36
|
+
def infer_ref(ref, argument, field)
|
37
|
+
type = @schema.types[field.type.unpack]
|
38
|
+
arg = type.arguments[argument.name]
|
39
|
+
raise ArgumentNotFound, "#{type.name}.#{field.name}(#{argument.name})" unless arg
|
40
|
+
|
41
|
+
ref.inferred_type = Syntax::Type.from_type_ref(arg.type)
|
42
|
+
end
|
43
|
+
|
44
|
+
def infer_field(field, parent, document)
|
45
|
+
name = object_name(parent, document)
|
46
|
+
object_type = type(name)
|
47
|
+
|
48
|
+
raise FieldNotFound, "#{object_name}.#{field.name}" unless object_type.fields.key?(field.name)
|
49
|
+
|
50
|
+
field.type = Syntax::Type.from_type_ref(object_type.fields[field.name].type)
|
51
|
+
end
|
52
|
+
|
53
|
+
def type(name)
|
54
|
+
object_type = @schema.types[name]
|
55
|
+
raise TypeNotFound, object_name unless object_type
|
56
|
+
|
57
|
+
object_type
|
58
|
+
end
|
59
|
+
|
60
|
+
def object_name(parent, document)
|
61
|
+
case parent
|
62
|
+
when Syntax::FragmentSpread
|
63
|
+
fragment = document.fragments[parent.name]
|
64
|
+
raise CannotDetermineTypeName, parent unless fragment
|
65
|
+
|
66
|
+
fragment.type_name
|
67
|
+
when Syntax::InlineFragment, Syntax::Fragment
|
68
|
+
parent.type_name
|
69
|
+
when Syntax::Field
|
70
|
+
parent.type.unpack
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
require_relative '../syntax'
|
5
|
+
require_relative './inline_fragments'
|
6
|
+
|
7
|
+
module Graphlyte
|
8
|
+
module Editors
|
9
|
+
# Reduce the query to a canonical form.
|
10
|
+
class Canonicalize
|
11
|
+
def edit(doc)
|
12
|
+
doc = doc.dup
|
13
|
+
InlineFragments.new.edit(doc)
|
14
|
+
doc.definitions = doc.definitions.sort_by(&:name)
|
15
|
+
# TODO: we should also perform the selection Merge operation here.
|
16
|
+
order_argments.edit(doc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def order_argments
|
20
|
+
Editor.new
|
21
|
+
.on_field { |field| field.arguments = field.arguments&.sort_by(&:name) }
|
22
|
+
.on_directive { |dir| dir.arguments = dir.arguments&.sort_by(&:name) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|