graphlyte 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|