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