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.
- 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,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
|
6
|
-
to_s.
|
7
|
+
refine Symbol do
|
8
|
+
def camelize
|
9
|
+
to_s.camelize
|
7
10
|
end
|
8
11
|
end
|
9
|
-
|
10
|
-
|
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(
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
kind
|
47
|
-
name
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|