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