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,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ module Graphlyte
6
+ module Parsing
7
+ # Basic back-tracking parser, with parser-combinator methods and exceptional
8
+ # control-flow.
9
+ #
10
+ # This class is just scaffolding - all domain specific parsing should
11
+ # go in subclasses.
12
+ class BacktrackingParser
13
+ attr_reader :tokens
14
+ attr_accessor :max_depth
15
+
16
+ def initialize(tokens:, max_depth: nil)
17
+ @tokens = tokens
18
+ @index = -1
19
+ @max_depth = max_depth
20
+ @current_depth = 0
21
+ end
22
+
23
+ def inspect
24
+ "#<#{self.class} @index=#{@index} @current=#{current.inspect} ...>"
25
+ end
26
+
27
+ def to_s
28
+ inspect
29
+ end
30
+
31
+ def peek(offset: 0)
32
+ @tokens[@index + offset] || raise("No token at #{@index + offset}")
33
+ end
34
+
35
+ def current
36
+ @current ||= peek
37
+ end
38
+
39
+ def advance
40
+ @current = nil
41
+ @index += 1
42
+ end
43
+
44
+ def next_token
45
+ advance
46
+ current
47
+ end
48
+
49
+ def expect(type, value = nil)
50
+ try_parse do
51
+ t = next_token
52
+
53
+ if value
54
+ raise Expected.new(t, expected: value) unless t.type == type && t.value == value
55
+ else
56
+ raise Unexpected, t unless t.type == type
57
+ end
58
+
59
+ t.value
60
+ end
61
+ end
62
+
63
+ def optional(&block)
64
+ try_parse(&block)
65
+ rescue ParseError, IllegalValue
66
+ nil
67
+ end
68
+
69
+ def optional_list(&block)
70
+ optional(&block) || []
71
+ end
72
+
73
+ def many(limit: nil, &block)
74
+ ret = []
75
+
76
+ until ret.length == limit
77
+ begin
78
+ ret << try_parse(&block)
79
+ rescue ParseError
80
+ return ret
81
+ end
82
+ end
83
+
84
+ ret
85
+ end
86
+
87
+ def some(&block)
88
+ one = yield
89
+ rest = many(&block)
90
+
91
+ [one] + rest
92
+ end
93
+
94
+ def try_parse
95
+ idx = @index
96
+ yield
97
+ rescue ParseError => e
98
+ @index = idx
99
+ raise e
100
+ rescue IllegalValue => e
101
+ t = current
102
+ @index = idx
103
+ raise Illegal, t, e.message
104
+ end
105
+
106
+ def one_of(*alternatives)
107
+ err = nil
108
+ all_symbols = alternatives.all? { _1.is_a?(Symbol) }
109
+
110
+ alternatives.each do |alt|
111
+ case alt
112
+ when Symbol then return try_parse { send(alt) }
113
+ when Proc then return try_parse { alt.call }
114
+ else
115
+ raise 'Not an alternative'
116
+ end
117
+ rescue ParseError => e
118
+ err = e
119
+ end
120
+
121
+ raise ParseError, "At #{current.location}: Expected one of #{alternatives.join(', ')}" if err && all_symbols
122
+ raise err if err
123
+ end
124
+
125
+ def punctuator(token)
126
+ expect(:PUNCTUATOR, token)
127
+ end
128
+
129
+ def name(value = nil)
130
+ expect(:NAME, value)
131
+ end
132
+
133
+ def bracket(lhs, rhs, &block)
134
+ expect(:PUNCTUATOR, lhs)
135
+ raise TooDeep, current.location if too_deep?
136
+
137
+ ret = subfeature(&block)
138
+
139
+ expect(:PUNCTUATOR, rhs)
140
+
141
+ ret
142
+ end
143
+
144
+ def too_deep?
145
+ return false if max_depth.nil?
146
+
147
+ @current_depth > max_depth
148
+ end
149
+
150
+ def subfeature
151
+ d = @current_depth
152
+ @current_depth += 1
153
+
154
+ yield
155
+ ensure
156
+ @current_depth = d
157
+ end
158
+ end
159
+ end
160
+ end
@@ -1,22 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Graphlyte
2
4
  module Refinements
5
+ # Adds `camelize` methods to `Symbol` and `String`.
3
6
  module StringRefinement
4
- refine Symbol do
5
- def to_camel_case
6
- to_s.to_camel_case
7
+ refine Symbol do
8
+ def camelize
9
+ to_s.camelize
7
10
  end
8
11
  end
9
- refine String do
10
- def to_camel_case
12
+
13
+ refine String do
14
+ def camelize
15
+ return self if match(/^_*$/)
16
+
11
17
  start_of_string = match(/(^_+)/)&.[](0)
12
18
  end_of_string = match(/(_+$)/)&.[](0)
13
19
 
14
- middle = split("_").reject(&:empty?).inject([]) do |memo, str|
20
+ middle = split('_').reject(&:empty?).inject([]) do |memo, str|
15
21
  memo << (memo.empty? ? str : str.capitalize)
16
- end.join("")
22
+ end.join
17
23
 
18
24
  "#{start_of_string}#{middle}#{end_of_string}"
19
- end
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graphlyte
4
+ module Refinements
5
+ # Adds `to_input_value` method to classes we can interpret as an input value.
6
+ module SyntaxRefinements
7
+ refine Hash do
8
+ def to_input_value
9
+ transform_keys(&:to_s).transform_values(&:to_input_value)
10
+ end
11
+ end
12
+
13
+ refine Array do
14
+ def to_input_value
15
+ map(&:to_input_value)
16
+ end
17
+ end
18
+
19
+ refine String do
20
+ def to_input_value
21
+ Syntax::Value.new(self, :STRING)
22
+ end
23
+ end
24
+
25
+ refine Symbol do
26
+ def to_input_value
27
+ Syntax::Value.new(self, :ENUM)
28
+ end
29
+ end
30
+
31
+ refine Integer do
32
+ def to_input_value
33
+ Syntax::Value.new(self, :NUMBER)
34
+ end
35
+ end
36
+
37
+ refine Float do
38
+ def to_input_value
39
+ Syntax::Value.new(self, :NUMBER)
40
+ end
41
+ end
42
+
43
+ refine TrueClass do
44
+ def to_input_value
45
+ Syntax::Value.new(Syntax::TRUE, :BOOL)
46
+ end
47
+ end
48
+
49
+ refine FalseClass do
50
+ def to_input_value
51
+ Syntax::Value.new(Syntax::FALSE, :BOOL)
52
+ end
53
+ end
54
+
55
+ refine NilClass do
56
+ def to_input_value
57
+ Syntax::Value.new(Syntax::NULL, :NULL)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './data'
4
+
5
+ module Graphlyte
6
+ # Represents a schema definition, containing all type definitions available in a server
7
+ # Reflects the response to a [schema query](./schema_query.rb)
8
+ class Schema < Graphlyte::Data
9
+ # A directive adds metadata to a defintion.
10
+ # See: https://spec.graphql.org/October2021/#sec-Language.Directives
11
+ class Directive < Graphlyte::Data
12
+ attr_accessor :description, :name
13
+ attr_reader :arguments
14
+
15
+ def initialize(**)
16
+ super
17
+
18
+ @arguments ||= {}
19
+ end
20
+
21
+ def self.from_schema_response(data)
22
+ new(
23
+ name: data['name'],
24
+ description: data['description'],
25
+ arguments: Schema.entity_map(InputValue, data['arguments'])
26
+ )
27
+ end
28
+ end
29
+
30
+ # An input value defines the values of arguments and the fields of input objects.
31
+ class InputValue < Graphlyte::Data
32
+ attr_accessor :name, :description, :type, :default_value
33
+
34
+ def self.from_schema_response(data)
35
+ value = new
36
+
37
+ value.name = data['name']
38
+ value.description = data['description']
39
+ value.default_value = data['defaultValue']
40
+ value.type = TypeRef.from_schema_response(data['type'])
41
+
42
+ value
43
+ end
44
+ end
45
+
46
+ # A type ref names a run-time type
47
+ # See `Type` for the full type definition.
48
+ class TypeRef < Graphlyte::Data
49
+ attr_accessor :kind, :name, :of_type
50
+
51
+ def self.from_schema_response(data)
52
+ return unless data
53
+
54
+ ref = new
55
+
56
+ ref.name = data['name']
57
+ ref.kind = data['kind'].to_sym
58
+ ref.of_type = TypeRef.from_schema_response(data['ofType'])
59
+
60
+ ref
61
+ end
62
+
63
+ def unpack
64
+ return of_type.unpack if of_type
65
+
66
+ name
67
+ end
68
+ end
69
+
70
+ # The description of an enum member
71
+ class Enum < Graphlyte::Data
72
+ attr_accessor :name, :description, :is_deprecated, :deprecation_reason
73
+
74
+ def self.from_schema_response(data)
75
+ new(**data)
76
+ end
77
+ end
78
+
79
+ # A full type definition.
80
+ class Type < Graphlyte::Data
81
+ attr_accessor :kind, :name, :description
82
+ attr_reader :fields, :input_fields, :interfaces, :enums, :possible_types
83
+
84
+ def initialize(**)
85
+ super
86
+ @fields ||= {}
87
+ @input_fields ||= {}
88
+ @interfaces ||= []
89
+ @enums ||= {}
90
+ @possible_types ||= []
91
+ end
92
+
93
+ def self.from_schema_response(data)
94
+ new(
95
+ kind: data['kind'].to_sym,
96
+ name: data['name'],
97
+ description: data['description'],
98
+ fields: Schema.entity_map(Field, data['fields']),
99
+ input_fields: Schema.entity_map(InputValue, data['inputFields']),
100
+ enums: Schema.entity_map(Enum, data['enumValues']),
101
+ interfaces: Schema.entity_list(TypeRef, data['interfaces']),
102
+ possible_types: Schema.entity_list(TypeRef, data['possibleTypes'])
103
+ )
104
+ end
105
+ end
106
+
107
+ # A field definition
108
+ class Field < Graphlyte::Data
109
+ attr_accessor :name, :description, :type, :is_deprecated, :deprecation_reason
110
+ attr_reader :arguments
111
+
112
+ def initialize(**)
113
+ super
114
+
115
+ @arguments ||= {}
116
+ end
117
+
118
+ def self.from_schema_response(data)
119
+ new(
120
+ name: data['name'],
121
+ description: data['description'],
122
+ type: TypeRef.from_schema_response(data['type']),
123
+ is_deprecated: data['isDeprecated'],
124
+ deprecation_reason: data['deprecationReason'],
125
+ arguments: Schema.entity_map(InputValue, data['arguments'])
126
+ )
127
+ end
128
+ end
129
+
130
+ attr_accessor :query_type, :mutation_type, :subscription_type
131
+ attr_reader :types, :directives
132
+
133
+ def initialize(**)
134
+ super
135
+
136
+ @types ||= {}
137
+ @directives ||= {}
138
+ end
139
+
140
+ def self.from_schema_response(response)
141
+ data = response.dig('data', '__schema')
142
+ raise Argument, 'No data' unless data
143
+
144
+ new(
145
+ query_type: data.dig('queryType', 'name'),
146
+ mutation_type: data.dig('queryType', 'name'),
147
+ subscription_type: data.dig('subscriptionType', 'name'),
148
+ types: entity_map(Type, data['types']),
149
+ directives: entity_map(Directive, data['directives'])
150
+ )
151
+ end
152
+
153
+ def self.entity_list(entity, resp)
154
+ return unless resp
155
+
156
+ resp.map { entity.from_schema_response(_1) }
157
+ end
158
+
159
+ def self.entity_map(entity, resp)
160
+ return unless resp
161
+
162
+ resp.to_h { |entry| [entry['name'], entity.from_schema_response(entry)] }
163
+ end
164
+ end
165
+ end
@@ -1,83 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './dsl'
1
4
 
2
5
  module Graphlyte
6
+ # extend this module to gain access to a schema_query method
3
7
  module SchemaQuery
4
8
  def schema_query
5
- type_ref_fragment = Graphlyte.fragment('TypeRef', '__Type') do
6
- kind
7
- name
8
- of_type {
9
- kind
10
- name
11
- of_type {
12
- kind
13
- name
14
- of_type {
15
- kind
16
- name
17
- of_type {
18
- kind
19
- name
20
- of_type {
21
- kind
22
- name
23
- of_type {
24
- kind
25
- name
26
- of_type {
27
- kind
28
- name
29
- }
30
- }
31
- }
32
- }
33
- }
34
- }
35
- }
9
+ SchemaQuery::Query.new.build
10
+ end
11
+
12
+ # Builder for a schema query
13
+ class Query
14
+ attr_reader :dsl, :doc
15
+
16
+ def initialize
17
+ @dsl = DSL.new
18
+ @doc = Graphlyte::Document.new
19
+ end
20
+
21
+ def build
22
+ @build ||= dsl.query('Schema', doc) do |q|
23
+ q.__schema do |s|
24
+ s.query_type(&:name)
25
+ s.mutation_type(&:name)
26
+ s.subscription_type(&:name)
27
+ s.types(full_type_fragment)
28
+ query_directives(s)
29
+ end
30
+ end
31
+ end
32
+
33
+ def query_directives(schema)
34
+ schema.directives do |d|
35
+ d.name
36
+ d.description
37
+ d.args(input_value_fragment)
38
+ end
36
39
  end
37
40
 
38
- input_value_fragment = Graphlyte.fragment('InputValues', '__InputValue') do
39
- name
40
- description
41
- type type_ref_fragment
42
- default_value
41
+ def type_ref_fragment
42
+ @type_ref_fragment ||= dsl.fragment(on: '__Type', doc: doc) do |t|
43
+ select_type_reference(t)
44
+ end
43
45
  end
44
46
 
45
- full_type_fragment = Graphlyte.fragment('FullType', '__Type') do
46
- kind
47
- name
48
- description
49
- fields(includeDeprecated: true) do
50
- name
51
- description
52
- args input_value_fragment
53
- type type_ref_fragment
54
- is_deprecated
55
- deprecation_reason
47
+ def select_type_reference(node, depth: 0)
48
+ node.kind
49
+ node.name
50
+ node.of_type { |child| select_type_reference(child, depth: depth + 1) } if depth < 8
51
+ end
52
+
53
+ def full_type_fragment
54
+ @full_type_fragment ||= dsl.fragment(on: '__Type', doc: doc) do |t|
55
+ t.kind
56
+ t.name
57
+ t.description
58
+ t << fields_fragment
59
+ t.input_fields(input_value_fragment)
60
+ t.interfaces(type_ref_fragment)
61
+ t << enums_fragment
62
+ t.possible_types(type_ref_fragment)
56
63
  end
57
- input_fields input_value_fragment
58
- interfaces type_ref_fragment
59
- enum_values(includeDeprecated: true) do
60
- name
61
- description
62
- is_deprecated
63
- deprecation_reason
64
+ end
65
+
66
+ def enums_fragment
67
+ @enums_fragment ||= dsl.fragment(on: '__Type', doc: doc) do |t|
68
+ t.enum_values(include_deprecated: true) do |e|
69
+ e.name
70
+ e.description
71
+ e.is_deprecated
72
+ e.deprecation_reason
73
+ end
64
74
  end
65
- possible_types type_ref_fragment
66
75
  end
67
76
 
68
- Graphlyte.query do
69
- __schema do
70
- query_type { name }
71
- mutation_type { name }
72
- subscription_type { name }
73
- types full_type_fragment
74
- directives do
75
- name
76
- description
77
- args input_value_fragment
77
+ def fields_fragment
78
+ @fields_fragment ||= dsl.fragment(on: '__Type', doc: doc) do |t|
79
+ t.fields(include_deprecated: true) do |f|
80
+ f.name
81
+ f.description
82
+ f.args(input_value_fragment)
83
+ f.type(type_ref_fragment)
84
+ f.is_deprecated
85
+ f.deprecation_reason
78
86
  end
79
87
  end
80
88
  end
89
+
90
+ def input_value_fragment
91
+ @input_value_fragment ||= dsl.fragment(on: '__InputValue', doc: doc) do |v|
92
+ v.name
93
+ v.description
94
+ v.type type_ref_fragment
95
+ v.default_value
96
+ end
97
+ end
81
98
  end
82
99
  end
83
100
  end