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,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require_relative '../editor'
|
6
|
+
require_relative './inline_fragments'
|
7
|
+
|
8
|
+
module Graphlyte
|
9
|
+
module Editors
|
10
|
+
# Find all variable references in a document
|
11
|
+
class CollectVariableReferences
|
12
|
+
attr_reader :references
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@references = { Syntax::Operation => {}, Syntax::Fragment => {} }
|
16
|
+
end
|
17
|
+
|
18
|
+
def edit(doc)
|
19
|
+
doc = doc.dup
|
20
|
+
|
21
|
+
InlineFragments.new.edit(doc)
|
22
|
+
collector.edit(doc)
|
23
|
+
|
24
|
+
references
|
25
|
+
end
|
26
|
+
|
27
|
+
def collector
|
28
|
+
Editor.new.on_variable_reference do |ref, action|
|
29
|
+
d = action.definition
|
30
|
+
references[d.class][d.name] ||= [].to_set
|
31
|
+
references[d.class][d.name] << ref
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
require_relative '../syntax'
|
5
|
+
require_relative './collect_variable_references'
|
6
|
+
require_relative './with_variables'
|
7
|
+
|
8
|
+
module Graphlyte
|
9
|
+
module Editors
|
10
|
+
# Subclass of the full variable inference editor that runs
|
11
|
+
# solely on static information (no knowledge of variable
|
12
|
+
# runtime values or the exact selected operation).
|
13
|
+
#
|
14
|
+
# The main difference is that we are more lenient about raising.
|
15
|
+
class InferSignature < WithVariables
|
16
|
+
def initialize(schema = nil)
|
17
|
+
super(schema, nil, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def select_operation(_doc)
|
21
|
+
# no-op
|
22
|
+
end
|
23
|
+
|
24
|
+
# We should *always* be able to infer if there is a schema
|
25
|
+
# But if we are in dynamic mode, defer inferrence errors until
|
26
|
+
# we have runtime values (see `WithVariables`)
|
27
|
+
def cannot_infer!(ref)
|
28
|
+
super if @schema
|
29
|
+
end
|
30
|
+
|
31
|
+
def runtime_type_of(_ref)
|
32
|
+
# no-op
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
|
5
|
+
module Graphlyte
|
6
|
+
module Editors
|
7
|
+
# Replace all uses of fragment spreads by inlining the fragment. This will
|
8
|
+
# increase the size of the document, sometimes by rather a lot.
|
9
|
+
#
|
10
|
+
# But doing so then makes other analysis (such as variable usage) much simpler.
|
11
|
+
class InlineFragments
|
12
|
+
FragmentNotFound = Class.new(StandardError)
|
13
|
+
|
14
|
+
def edit(doc)
|
15
|
+
inliner.edit(doc)
|
16
|
+
defragmenter.edit(doc)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def inliner
|
22
|
+
@inliner ||= Editor.new.on_fragment_spread do |spread, action|
|
23
|
+
fragment = action.document.fragments[spread.name]
|
24
|
+
raise FragmentNotFound, spread.name unless fragment
|
25
|
+
|
26
|
+
action.replace fragment.inline
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def defragmenter
|
31
|
+
@defragmenter ||= Editor.new.on_fragment_definition do |_, action|
|
32
|
+
action.delete
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
require_relative '../syntax'
|
5
|
+
require_relative './annotate_types'
|
6
|
+
|
7
|
+
module Graphlyte
|
8
|
+
module Editors
|
9
|
+
# Remove unnecessary spreads.
|
10
|
+
#
|
11
|
+
# For example in the query:
|
12
|
+
#
|
13
|
+
# ```
|
14
|
+
# query {
|
15
|
+
# User(id: 1) {
|
16
|
+
# ... on User {
|
17
|
+
# name
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
# }
|
21
|
+
# ```
|
22
|
+
#
|
23
|
+
# If the `Query.User` field has the type `User` then the spread `... on User` is
|
24
|
+
# tautological, and we can replace this with:
|
25
|
+
#
|
26
|
+
# ```
|
27
|
+
# query {
|
28
|
+
# User(id: 1) {
|
29
|
+
# name
|
30
|
+
# }
|
31
|
+
# }
|
32
|
+
# ```
|
33
|
+
class RemoveUnneededSpreads
|
34
|
+
def initialize(schema = nil)
|
35
|
+
@schema = schema
|
36
|
+
end
|
37
|
+
|
38
|
+
def edit(doc)
|
39
|
+
AnnotateTypes.new(@schema).edit(doc)
|
40
|
+
|
41
|
+
inliner.edit(doc)
|
42
|
+
end
|
43
|
+
|
44
|
+
def inliner
|
45
|
+
@inliner ||= Editor.top_down.on_inline_fragment do |frag, action|
|
46
|
+
action.expand(frag.selection) if inlinable?(frag, action.parent)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def inlinable?(fragment, parent)
|
51
|
+
fragment.directives.none? && type_of(parent) == fragment.type_name
|
52
|
+
end
|
53
|
+
|
54
|
+
def type_of(node)
|
55
|
+
case node
|
56
|
+
when Syntax::Field
|
57
|
+
node.type&.inner
|
58
|
+
when Syntax::InlineFragment
|
59
|
+
node.type_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
require_relative '../syntax'
|
5
|
+
|
6
|
+
module Graphlyte
|
7
|
+
module Editors
|
8
|
+
# Reduce a document down to a single operation, removing unnecessary fragments
|
9
|
+
#
|
10
|
+
# eg:
|
11
|
+
#
|
12
|
+
# pry(main)> puts doc
|
13
|
+
#
|
14
|
+
# query A {
|
15
|
+
# foo(bar: $baz) {
|
16
|
+
# ...foos
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
# fragment foos on Foo {
|
21
|
+
# a
|
22
|
+
# b
|
23
|
+
# ...bars
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# fragment bars on Foo { d e f }
|
27
|
+
#
|
28
|
+
# query B {
|
29
|
+
# foo {
|
30
|
+
# ...bars
|
31
|
+
# }
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
# pry(main)> puts Graphlyte::Editors::SelectOperation.new('A').edit(doc.dup)
|
35
|
+
#
|
36
|
+
# query A {
|
37
|
+
# foo(bar: $baz) {
|
38
|
+
# ...foos
|
39
|
+
# }
|
40
|
+
# }
|
41
|
+
#
|
42
|
+
# fragment foos on Foo {
|
43
|
+
# a
|
44
|
+
# b
|
45
|
+
# ...bars
|
46
|
+
# }
|
47
|
+
#
|
48
|
+
# fragment bars on Foo { d e f }
|
49
|
+
#
|
50
|
+
# pry(main)> puts Graphlyte::Editors::SelectOperation.new('B').edit(doc.dup)
|
51
|
+
#
|
52
|
+
# fragment bars on Foo { d e f }
|
53
|
+
#
|
54
|
+
# query B {
|
55
|
+
# foo {
|
56
|
+
# ...bars
|
57
|
+
# }
|
58
|
+
# }
|
59
|
+
#
|
60
|
+
class SelectOperation
|
61
|
+
def initialize(operation)
|
62
|
+
@operation = operation
|
63
|
+
end
|
64
|
+
|
65
|
+
def edit(doc)
|
66
|
+
to_keep = build_fragment_tree(doc)[@operation]
|
67
|
+
|
68
|
+
doc.definitions.select! do |definition|
|
69
|
+
case definition
|
70
|
+
when Syntax::Operation
|
71
|
+
definition.name == @operation
|
72
|
+
else
|
73
|
+
to_keep.include?(definition.name)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
doc
|
78
|
+
end
|
79
|
+
|
80
|
+
# Compute the transitive closure of fragments used in each operation.
|
81
|
+
def build_fragment_tree(doc)
|
82
|
+
names = collect_fragment_names(doc)
|
83
|
+
|
84
|
+
# Promote fragments in fragments to the operation at the root
|
85
|
+
names[Syntax::Operation].each do |_op_name, spreads|
|
86
|
+
unvisited = spreads.to_a
|
87
|
+
|
88
|
+
until unvisited.empty?
|
89
|
+
names[Syntax::Fragment][unvisited.pop]&.each do |name|
|
90
|
+
next if spreads.include?(name)
|
91
|
+
|
92
|
+
spreads << name
|
93
|
+
unvisited << name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
names_per_op
|
99
|
+
end
|
100
|
+
|
101
|
+
def collect_fragment_names(doc)
|
102
|
+
names = { Syntax::Operation => {}, Syntax::Fragment => {} }
|
103
|
+
|
104
|
+
collect = Editor.new.on_fragment_spread do |spread, action|
|
105
|
+
set = (names[action.definition.class] ||= [].to_set)
|
106
|
+
|
107
|
+
set << spread.name
|
108
|
+
end
|
109
|
+
|
110
|
+
collect.edit(doc)
|
111
|
+
|
112
|
+
names
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../editor'
|
4
|
+
require_relative '../syntax'
|
5
|
+
require_relative './collect_variable_references'
|
6
|
+
require_relative './annotate_types'
|
7
|
+
|
8
|
+
module Graphlyte
|
9
|
+
module Editors
|
10
|
+
# Use variable values to infer missing variables in the query signature
|
11
|
+
class WithVariables
|
12
|
+
CannotInfer = Class.new(StandardError)
|
13
|
+
TypeMismatch = Class.new(StandardError)
|
14
|
+
|
15
|
+
def initialize(schema, operation, variables)
|
16
|
+
@schema = schema
|
17
|
+
@operation = operation
|
18
|
+
@variables = variables
|
19
|
+
end
|
20
|
+
|
21
|
+
def edit(doc)
|
22
|
+
select_operation(doc)
|
23
|
+
annotate_types(doc)
|
24
|
+
|
25
|
+
references = Editors::CollectVariableReferences.new.edit(doc)
|
26
|
+
editor = infer_variable_type(references)
|
27
|
+
|
28
|
+
editor.edit(doc)
|
29
|
+
end
|
30
|
+
|
31
|
+
def annotate_types(doc)
|
32
|
+
Editors::AnnotateTypes.new(@schema).edit(doc) if @schema
|
33
|
+
end
|
34
|
+
|
35
|
+
def select_operation(doc)
|
36
|
+
Editors::SelectOperation.new(@operation).edit(doc) if @operation
|
37
|
+
end
|
38
|
+
|
39
|
+
def infer_variable_type(references)
|
40
|
+
Editor.new.on_operation do |operation, editor|
|
41
|
+
refs = references[operation.class][operation.name]
|
42
|
+
next unless refs
|
43
|
+
|
44
|
+
infer_operation(operation, refs, editor.document)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def infer_operation(operation, refs, document)
|
49
|
+
current_vars = current_operation_variables(operation)
|
50
|
+
|
51
|
+
added = {}
|
52
|
+
refs.to_a.reject { current_vars[_1.variable] }.each do |ref|
|
53
|
+
# Only way this could happen is if `uniq` produces duplicate names
|
54
|
+
# And that can only happen if there are two types inferred
|
55
|
+
# for the same reference.
|
56
|
+
if (prev = added[ref.variable])
|
57
|
+
raise TypeMismatch, "#{ref.variable}: #{ref.inferred_type} != #{prev.type}"
|
58
|
+
end
|
59
|
+
|
60
|
+
infer(operation.variables, added, document, ref)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_operation_variables(operation)
|
65
|
+
operation.variables ||= []
|
66
|
+
operation.variables.to_h { [_1.variable, _1] }
|
67
|
+
end
|
68
|
+
|
69
|
+
def infer(variables, added, doc, ref)
|
70
|
+
var = doc.variables.fetch(ref.variable, ref.to_definition)
|
71
|
+
type = var.type || ref.inferred_type || runtime_type_of(ref)
|
72
|
+
|
73
|
+
if type
|
74
|
+
var.type ||= type
|
75
|
+
variables << var
|
76
|
+
added[ref.variable] = var
|
77
|
+
else
|
78
|
+
cannot_infer!(ref)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def cannot_infer!(ref)
|
83
|
+
raise CannotInfer, ref.variable
|
84
|
+
end
|
85
|
+
|
86
|
+
def runtime_type_of(ref)
|
87
|
+
value = @variables[ref.variable]
|
88
|
+
|
89
|
+
case value
|
90
|
+
when Integer
|
91
|
+
Syntax::Type.non_null('Int')
|
92
|
+
when Float
|
93
|
+
Syntax::Type.non_null('Float')
|
94
|
+
when String
|
95
|
+
Syntax::Type.non_null('String')
|
96
|
+
when Date
|
97
|
+
Syntax::Type.non_null('Date')
|
98
|
+
when TrueClass, FalseClass
|
99
|
+
Syntax::Type.non_null('Boolean')
|
100
|
+
when Array
|
101
|
+
Syntax::Type.list_of(runtime_type_of(value.first)) unless value.empty?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Graphlyte
|
4
|
+
IllegalValue = Class.new(ArgumentError)
|
5
|
+
|
6
|
+
ParseError = Class.new(StandardError)
|
7
|
+
|
8
|
+
TooDeep = Class.new(StandardError) do
|
9
|
+
def initialize(location)
|
10
|
+
super("Max parse depth exceeded at #{location}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Unexpected = Class.new(ParseError) do
|
15
|
+
def initialize(token)
|
16
|
+
super("Unexpected token at #{token.location}: #{token.lexeme.inspect}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Expected = Class.new(ParseError) do
|
21
|
+
def initialize(token, expected:)
|
22
|
+
super("Unexpected token at #{token.location}: #{token.lexeme.inspect}, expected #{expected}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Illegal = Class.new(ParseError) do
|
27
|
+
def initialize(token, reason = nil)
|
28
|
+
msg = "Illegal token at #{token.location}: #{token.lexeme}"
|
29
|
+
msg << ", #{reason}" if reason
|
30
|
+
super(msg)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|