graphlyte 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphlyte/data.rb +68 -0
  3. data/lib/graphlyte/document.rb +131 -0
  4. data/lib/graphlyte/dsl.rb +86 -0
  5. data/lib/graphlyte/editor.rb +288 -0
  6. data/lib/graphlyte/editors/annotate_types.rb +75 -0
  7. data/lib/graphlyte/editors/canonicalize.rb +26 -0
  8. data/lib/graphlyte/editors/collect_variable_references.rb +36 -0
  9. data/lib/graphlyte/editors/infer_signature.rb +36 -0
  10. data/lib/graphlyte/editors/inline_fragments.rb +37 -0
  11. data/lib/graphlyte/editors/remove_unneeded_spreads.rb +64 -0
  12. data/lib/graphlyte/editors/select_operation.rb +116 -0
  13. data/lib/graphlyte/editors/with_variables.rb +106 -0
  14. data/lib/graphlyte/errors.rb +33 -0
  15. data/lib/graphlyte/lexer.rb +392 -0
  16. data/lib/graphlyte/lexing/location.rb +43 -0
  17. data/lib/graphlyte/lexing/token.rb +31 -0
  18. data/lib/graphlyte/parser.rb +269 -0
  19. data/lib/graphlyte/parsing/backtracking_parser.rb +160 -0
  20. data/lib/graphlyte/refinements/string_refinement.rb +14 -8
  21. data/lib/graphlyte/refinements/syntax_refinements.rb +62 -0
  22. data/lib/graphlyte/schema.rb +165 -0
  23. data/lib/graphlyte/schema_query.rb +82 -65
  24. data/lib/graphlyte/selection_builder.rb +189 -0
  25. data/lib/graphlyte/selector.rb +75 -0
  26. data/lib/graphlyte/serializer.rb +223 -0
  27. data/lib/graphlyte/syntax.rb +369 -0
  28. data/lib/graphlyte.rb +24 -42
  29. metadata +88 -18
  30. data/lib/graphlyte/arguments/set.rb +0 -88
  31. data/lib/graphlyte/arguments/value.rb +0 -32
  32. data/lib/graphlyte/builder.rb +0 -53
  33. data/lib/graphlyte/directive.rb +0 -21
  34. data/lib/graphlyte/field.rb +0 -65
  35. data/lib/graphlyte/fieldset.rb +0 -36
  36. data/lib/graphlyte/fragment.rb +0 -17
  37. data/lib/graphlyte/inline_fragment.rb +0 -29
  38. data/lib/graphlyte/query.rb +0 -148
  39. data/lib/graphlyte/schema/parser.rb +0 -674
  40. data/lib/graphlyte/schema/types/base.rb +0 -54
  41. 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