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