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
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
|