graph_weaver 0.7.3 → 0.7.4
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/Gemfile.lock +2 -2
- data/docs/federation.md +3 -2
- data/docs/generated_modules.md +49 -8
- data/docs/getting_started.md +15 -0
- data/docs/i18n.md +4 -4
- data/docs/scalars.md +123 -32
- data/docs/testing.md +21 -2
- data/docs/upgrading.md +31 -5
- data/examples/github/generated/star_mutation.rb +24 -2
- data/examples/github/generated/stargazers_query.rb +61 -5
- data/examples/github/generated/starred_query.rb +33 -3
- data/lib/graph_weaver/client.rb +23 -0
- data/lib/graph_weaver/codegen/emit.rb +22 -7
- data/lib/graph_weaver/codegen/enum_type.rb +132 -19
- data/lib/graph_weaver/codegen/nodes.rb +20 -9
- data/lib/graph_weaver/codegen/scalar_type.rb +72 -18
- data/lib/graph_weaver/codegen/type_helpers.rb +71 -13
- data/lib/graph_weaver/codegen.rb +61 -16
- data/lib/graph_weaver/coerce.rb +24 -5
- data/lib/graph_weaver/graph.rb +4 -1
- data/lib/graph_weaver/hints.rb +4 -1
- data/lib/graph_weaver/input_struct.rb +13 -7
- data/lib/graph_weaver/internal/values.rb +5 -2
- data/lib/graph_weaver/internal.rb +67 -0
- data/lib/graph_weaver/testing.rb +101 -0
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +67 -67
- metadata +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Generated by GraphWeaver 0.7.
|
|
4
|
+
# Generated by GraphWeaver 0.7.4 — do not edit.
|
|
5
5
|
|
|
6
6
|
require "time"
|
|
7
7
|
|
|
@@ -85,6 +85,14 @@ module StargazersQuery
|
|
|
85
85
|
rescue StandardError => e
|
|
86
86
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
87
87
|
end
|
|
88
|
+
|
|
89
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
90
|
+
def as_json(*_options)
|
|
91
|
+
{
|
|
92
|
+
"nameWithOwner" => name_with_owner,
|
|
93
|
+
"stargazerCount" => stargazer_count,
|
|
94
|
+
}
|
|
95
|
+
end
|
|
88
96
|
end
|
|
89
97
|
|
|
90
98
|
const :nodes, T.nilable(T::Array[T.nilable(Nodes)])
|
|
@@ -99,6 +107,13 @@ module StargazersQuery
|
|
|
99
107
|
rescue StandardError => e
|
|
100
108
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
101
109
|
end
|
|
110
|
+
|
|
111
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
112
|
+
def as_json(*_options)
|
|
113
|
+
{
|
|
114
|
+
"nodes" => nodes&.then { |v1| v1.map { |v2| v2&.then { |v3| v3.as_json } } },
|
|
115
|
+
}
|
|
116
|
+
end
|
|
102
117
|
end
|
|
103
118
|
|
|
104
119
|
const :login, String
|
|
@@ -117,6 +132,15 @@ module StargazersQuery
|
|
|
117
132
|
rescue StandardError => e
|
|
118
133
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
119
134
|
end
|
|
135
|
+
|
|
136
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
137
|
+
def as_json(*_options)
|
|
138
|
+
{
|
|
139
|
+
"login" => login,
|
|
140
|
+
"name" => name,
|
|
141
|
+
"repositories" => repositories.as_json,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
120
144
|
end
|
|
121
145
|
|
|
122
146
|
const :starred_at, Time
|
|
@@ -125,7 +149,7 @@ module StargazersQuery
|
|
|
125
149
|
sig { params(data: T::Hash[String, T.untyped]).returns(Edges) }
|
|
126
150
|
def self.from_h(data)
|
|
127
151
|
new(
|
|
128
|
-
starred_at: GraphWeaver::Hints.field(self, "starredAt") { Time.
|
|
152
|
+
starred_at: GraphWeaver::Hints.field(self, "starredAt") { Time.iso8601(data.fetch("starredAt")) },
|
|
129
153
|
node: Node.from_h(data.fetch("node")),
|
|
130
154
|
)
|
|
131
155
|
rescue GraphWeaver::Error
|
|
@@ -133,6 +157,14 @@ module StargazersQuery
|
|
|
133
157
|
rescue StandardError => e
|
|
134
158
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
135
159
|
end
|
|
160
|
+
|
|
161
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
162
|
+
def as_json(*_options)
|
|
163
|
+
{
|
|
164
|
+
"starredAt" => GraphWeaver::Coerce.timestamp(starred_at),
|
|
165
|
+
"node" => node.as_json,
|
|
166
|
+
}
|
|
167
|
+
end
|
|
136
168
|
end
|
|
137
169
|
|
|
138
170
|
const :edges, T.nilable(T::Array[T.nilable(Edges)])
|
|
@@ -147,6 +179,13 @@ module StargazersQuery
|
|
|
147
179
|
rescue StandardError => e
|
|
148
180
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
149
181
|
end
|
|
182
|
+
|
|
183
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
184
|
+
def as_json(*_options)
|
|
185
|
+
{
|
|
186
|
+
"edges" => edges&.then { |v1| v1.map { |v2| v2&.then { |v3| v3.as_json } } },
|
|
187
|
+
}
|
|
188
|
+
end
|
|
150
189
|
end
|
|
151
190
|
|
|
152
191
|
const :id, String
|
|
@@ -167,6 +206,16 @@ module StargazersQuery
|
|
|
167
206
|
rescue StandardError => e
|
|
168
207
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
169
208
|
end
|
|
209
|
+
|
|
210
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
211
|
+
def as_json(*_options)
|
|
212
|
+
{
|
|
213
|
+
"id" => id,
|
|
214
|
+
"nameWithOwner" => name_with_owner,
|
|
215
|
+
"stargazerCount" => stargazer_count,
|
|
216
|
+
"stargazers" => stargazers.as_json,
|
|
217
|
+
}
|
|
218
|
+
end
|
|
170
219
|
end
|
|
171
220
|
|
|
172
221
|
const :repository, T.nilable(Repository)
|
|
@@ -181,6 +230,13 @@ module StargazersQuery
|
|
|
181
230
|
rescue StandardError => e
|
|
182
231
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
183
232
|
end
|
|
233
|
+
|
|
234
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
235
|
+
def as_json(*_options)
|
|
236
|
+
{
|
|
237
|
+
"repository" => repository&.then { |v1| v1.as_json },
|
|
238
|
+
}
|
|
239
|
+
end
|
|
184
240
|
end
|
|
185
241
|
|
|
186
242
|
# client — see GraphWeaver::QueryModule
|
|
@@ -196,9 +252,9 @@ module StargazersQuery
|
|
|
196
252
|
sig { params(owner: String, name: String, first: Integer, client: T.untyped).returns(GraphWeaver::Response[Result]).checked(:never) }
|
|
197
253
|
def self.execute(owner:, name:, first:, client: nil)
|
|
198
254
|
variables = {
|
|
199
|
-
"owner" => GraphWeaver::Coerce.variable("owner", OPERATION_NAME, owner) { |v| GraphWeaver::Coerce.string(v) },
|
|
200
|
-
"name" => GraphWeaver::Coerce.variable("name", OPERATION_NAME, name) { |v| GraphWeaver::Coerce.string(v) },
|
|
201
|
-
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v) },
|
|
255
|
+
"owner" => GraphWeaver::Coerce.variable("owner", OPERATION_NAME, owner) { |v| GraphWeaver::Coerce.string(v, "String") },
|
|
256
|
+
"name" => GraphWeaver::Coerce.variable("name", OPERATION_NAME, name) { |v| GraphWeaver::Coerce.string(v, "String") },
|
|
257
|
+
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v, "Int") },
|
|
202
258
|
}
|
|
203
259
|
|
|
204
260
|
from_response(dispatch(variables, client:))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Generated by GraphWeaver 0.7.
|
|
4
|
+
# Generated by GraphWeaver 0.7.4 — do not edit.
|
|
5
5
|
|
|
6
6
|
module StarredQuery
|
|
7
7
|
extend T::Sig
|
|
@@ -57,6 +57,14 @@ module StarredQuery
|
|
|
57
57
|
rescue StandardError => e
|
|
58
58
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
62
|
+
def as_json(*_options)
|
|
63
|
+
{
|
|
64
|
+
"nameWithOwner" => name_with_owner,
|
|
65
|
+
"stargazerCount" => stargazer_count,
|
|
66
|
+
}
|
|
67
|
+
end
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
const :total_count, Integer
|
|
@@ -73,6 +81,14 @@ module StarredQuery
|
|
|
73
81
|
rescue StandardError => e
|
|
74
82
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
75
83
|
end
|
|
84
|
+
|
|
85
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
86
|
+
def as_json(*_options)
|
|
87
|
+
{
|
|
88
|
+
"totalCount" => total_count,
|
|
89
|
+
"nodes" => nodes&.then { |v1| v1.map { |v2| v2&.then { |v3| v3.as_json } } },
|
|
90
|
+
}
|
|
91
|
+
end
|
|
76
92
|
end
|
|
77
93
|
|
|
78
94
|
const :starred_repositories, StarredRepositories
|
|
@@ -87,6 +103,13 @@ module StarredQuery
|
|
|
87
103
|
rescue StandardError => e
|
|
88
104
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
89
105
|
end
|
|
106
|
+
|
|
107
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
108
|
+
def as_json(*_options)
|
|
109
|
+
{
|
|
110
|
+
"starredRepositories" => starred_repositories.as_json,
|
|
111
|
+
}
|
|
112
|
+
end
|
|
90
113
|
end
|
|
91
114
|
|
|
92
115
|
const :user, T.nilable(User)
|
|
@@ -101,6 +124,13 @@ module StarredQuery
|
|
|
101
124
|
rescue StandardError => e
|
|
102
125
|
raise GraphWeaver::CastError.new(struct: self, message: GraphWeaver::Hints.cast_message(self, data, e))
|
|
103
126
|
end
|
|
127
|
+
|
|
128
|
+
sig { params(_options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
129
|
+
def as_json(*_options)
|
|
130
|
+
{
|
|
131
|
+
"user" => user&.then { |v1| v1.as_json },
|
|
132
|
+
}
|
|
133
|
+
end
|
|
104
134
|
end
|
|
105
135
|
|
|
106
136
|
# client — see GraphWeaver::QueryModule
|
|
@@ -116,8 +146,8 @@ module StarredQuery
|
|
|
116
146
|
sig { params(login: String, first: Integer, client: T.untyped).returns(GraphWeaver::Response[Result]).checked(:never) }
|
|
117
147
|
def self.execute(login:, first:, client: nil)
|
|
118
148
|
variables = {
|
|
119
|
-
"login" => GraphWeaver::Coerce.variable("login", OPERATION_NAME, login) { |v| GraphWeaver::Coerce.string(v) },
|
|
120
|
-
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v) },
|
|
149
|
+
"login" => GraphWeaver::Coerce.variable("login", OPERATION_NAME, login) { |v| GraphWeaver::Coerce.string(v, "String") },
|
|
150
|
+
"first" => GraphWeaver::Coerce.variable("first", OPERATION_NAME, first) { |v| GraphWeaver::Coerce.integer(v, "Int") },
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
from_response(dispatch(variables, client:))
|
data/lib/graph_weaver/client.rb
CHANGED
|
@@ -190,6 +190,29 @@ class GraphWeaver::Client
|
|
|
190
190
|
run(query, **variables).data!
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# Does this query validate? The string form of GraphWeaver.check_queries,
|
|
194
|
+
# answering with the same JSON-ready hashes — `message`, `line`, `column`,
|
|
195
|
+
# plus `subgraphs` where a supergraph brands them — so an empty array means
|
|
196
|
+
# it validates:
|
|
197
|
+
#
|
|
198
|
+
# client.check_query("query { viewer { login } }") # => []
|
|
199
|
+
# client.check_query("query { viewer { lgoin } }")
|
|
200
|
+
# # => [{ "message" => "Field 'lgoin' doesn't exist on type 'User'",
|
|
201
|
+
# # "line" => 1, "column" => 17 }]
|
|
202
|
+
#
|
|
203
|
+
# Checked against this client's own schema — what `execute` would run
|
|
204
|
+
# against — so a url client introspects on first use as it always does, and
|
|
205
|
+
# nothing re-introspects the way check_queries defaults to. An unparseable
|
|
206
|
+
# source is an entry like any other; nothing here raises for a bad query.
|
|
207
|
+
# Shared fragments are inlined from fragments: the same way every other door
|
|
208
|
+
# inlines them.
|
|
209
|
+
def check_query(source, fragments: GraphWeaver.fragments_paths)
|
|
210
|
+
GraphWeaver::Internal::QueryCheck.errors(
|
|
211
|
+
schema, source, GraphWeaver::Codegen.load_fragments(fragments),
|
|
212
|
+
GraphWeaver::Internal::QueryCheck.routing_table_for(schema_source),
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
193
216
|
private
|
|
194
217
|
|
|
195
218
|
# Anything already speaking the client contract — another Client,
|
|
@@ -97,7 +97,7 @@ class GraphWeaver::Codegen
|
|
|
97
97
|
# both reference it by that name.
|
|
98
98
|
def shared_enum_names
|
|
99
99
|
@mapped_enums.each_value.flat_map { |m| ["#{m.const_prefix}_FROM_WIRE", "#{m.const_prefix}_TO_WIRE"] } +
|
|
100
|
-
@enums.each_value.
|
|
100
|
+
@enums.each_value.flat_map { |e| e.aliased? ? [e.class_name, e.alias_const] : [e.class_name] }
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Inputs: only the variable root types — the names this module's own
|
|
@@ -340,9 +340,17 @@ class GraphWeaver::Codegen
|
|
|
340
340
|
end
|
|
341
341
|
out << "#{pad} end"
|
|
342
342
|
out << "#{pad}end"
|
|
343
|
+
return unless node.aliased?
|
|
344
|
+
|
|
345
|
+
out << "#{pad}# wire spellings read as another value of #{node.class_name} (register_enum alias:)"
|
|
346
|
+
out << "#{pad}#{node.alias_const} = T.let({"
|
|
347
|
+
node.aliases.each { |from, to| out << "#{pad} #{from.inspect} => #{to.inspect}," }
|
|
348
|
+
out << "#{pad}}.freeze, T::Hash[String, String])"
|
|
343
349
|
end
|
|
344
350
|
|
|
345
|
-
# module-level wire translation tables for an app-mapped enum
|
|
351
|
+
# module-level wire translation tables for an app-mapped enum. TO_WIRE is
|
|
352
|
+
# written out rather than inverted from FROM_WIRE: an alias puts two
|
|
353
|
+
# spellings on one member, and only one of them goes back out.
|
|
346
354
|
def emit_mapped_enum(node, out, indent)
|
|
347
355
|
pad = " " * indent
|
|
348
356
|
type = node.bare_type
|
|
@@ -354,7 +362,11 @@ class GraphWeaver::Codegen
|
|
|
354
362
|
out << "#{pad} #{wire.inspect} => #{type}.deserialize(#{member.serialize.to_s.inspect}),"
|
|
355
363
|
end
|
|
356
364
|
out << "#{pad}}.freeze, T::Hash[String, #{type}])"
|
|
357
|
-
out << "#{pad}#{prefix}_TO_WIRE = T.let(
|
|
365
|
+
out << "#{pad}#{prefix}_TO_WIRE = T.let({"
|
|
366
|
+
node.to_wire.each do |member, wire|
|
|
367
|
+
out << "#{pad} #{type}.deserialize(#{member.serialize.to_s.inspect}) => #{wire.inspect},"
|
|
368
|
+
end
|
|
369
|
+
out << "#{pad}}.freeze, T::Hash[#{type}, String])"
|
|
358
370
|
end
|
|
359
371
|
|
|
360
372
|
def emit_object(node, out, indent)
|
|
@@ -383,7 +395,10 @@ class GraphWeaver::Codegen
|
|
|
383
395
|
node.fields.each do |field|
|
|
384
396
|
note = renamed_note(field.prop, field.key)
|
|
385
397
|
out << "#{pad} #{note}" if note
|
|
386
|
-
|
|
398
|
+
# a prop satisfying a mixin's abstract sig has to declare the override,
|
|
399
|
+
# and a `const` says it as a rule — there is no sig to put it in
|
|
400
|
+
override = node.overrides.include?(field.prop) ? ", override: true" : ""
|
|
401
|
+
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{override}"
|
|
387
402
|
end
|
|
388
403
|
|
|
389
404
|
out << ""
|
|
@@ -418,7 +433,7 @@ class GraphWeaver::Codegen
|
|
|
418
433
|
# selected field onto the struct, next to the honest wire data
|
|
419
434
|
node.aliases.each do |a|
|
|
420
435
|
out << ""
|
|
421
|
-
out << "#{pad} sig { returns(#{a.type}) }"
|
|
436
|
+
out << "#{pad} sig { #{"override." if node.overrides.include?(a.name)}returns(#{a.type}) }"
|
|
422
437
|
out << "#{pad} def #{a.name} = #{a.expr}"
|
|
423
438
|
end
|
|
424
439
|
out << "#{pad}end"
|
|
@@ -677,13 +692,13 @@ class GraphWeaver::Codegen
|
|
|
677
692
|
out << "#{pad} const :#{field.prop}, #{type}#{default}"
|
|
678
693
|
end
|
|
679
694
|
out << ""
|
|
680
|
-
out << "#{pad} # (prop, wire, required, serializer, coercer, coordinate) per field"
|
|
695
|
+
out << "#{pad} # (prop, wire, required, serializer, coercer, coordinate, type) per field"
|
|
681
696
|
out << "#{pad} FIELDS = T.let(["
|
|
682
697
|
node.fields.each do |field|
|
|
683
698
|
serializer = field.node.serialize_identity? ? "nil" : "->(v) { #{field.node.serialize("v", 1)} }"
|
|
684
699
|
coercer = field.node.hash_coerce_identity? ? "nil" : "->(v) { #{field.node.hash_coerce("v", 1)} }"
|
|
685
700
|
coordinate = "#{node.graphql_name}.#{field.wire}"
|
|
686
|
-
out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}, #{coordinate.inspect}),"
|
|
701
|
+
out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}, #{coordinate.inspect}, #{field.type.inspect}),"
|
|
687
702
|
end
|
|
688
703
|
out << "#{pad} ].freeze, T::Array[GraphWeaver::InputStruct::Field])"
|
|
689
704
|
# InputStruct reads it with const_get, which privacy doesn't block
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
class GraphWeaver::Codegen
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# What a register_enum said about one GraphQL enum: the app-owned T::Enum
|
|
6
|
+
# its values map onto, and which wire spellings are the same value.
|
|
7
7
|
#
|
|
8
8
|
# class PetKind < T::Enum
|
|
9
9
|
# enums { Cat = new("cat"); Dog = new("dog") }
|
|
@@ -17,11 +17,23 @@ class GraphWeaver::Codegen
|
|
|
17
17
|
# value the schema declares must resolve — generation fails naming the
|
|
18
18
|
# gaps — unless fallback: names a member to absorb unknown values
|
|
19
19
|
# (forward-compat for servers that add members; inputs stay strict).
|
|
20
|
+
#
|
|
21
|
+
# alias: { "legacy_mode" => "LEGACY_MODE" } says two wire values are one
|
|
22
|
+
# value — both spellings cast, the target is what serializes. It is the
|
|
23
|
+
# whole registration when there is no T::Enum to map onto, and then the
|
|
24
|
+
# generated enum gets one constant for the target and none for the alias.
|
|
20
25
|
class EnumType
|
|
21
26
|
attr_reader :graphql_name, :type, :fallback, :requires
|
|
22
27
|
|
|
23
|
-
def initialize(graphql_name, type, map: nil, fallback: nil, requires: nil)
|
|
28
|
+
def initialize(graphql_name, type, map: nil, fallback: nil, requires: nil, aliases: nil)
|
|
24
29
|
@graphql_name = graphql_name.to_s
|
|
30
|
+
@aliases = normalize_aliases!(aliases)
|
|
31
|
+
@type = type
|
|
32
|
+
@map = map || {}
|
|
33
|
+
@fallback = fallback
|
|
34
|
+
|
|
35
|
+
return alias_only!(map, fallback, requires) if type.nil?
|
|
36
|
+
|
|
25
37
|
# A name, not the class, is what you write when the constant won't
|
|
26
38
|
# resolve yet — which in Rails means a config/initializers file, since
|
|
27
39
|
# autoloading is set up after those run. Say where it does resolve.
|
|
@@ -36,9 +48,6 @@ class GraphWeaver::Codegen
|
|
|
36
48
|
raise ArgumentError, "type: must be a named constant (anonymous classes can't appear in generated source)"
|
|
37
49
|
end
|
|
38
50
|
|
|
39
|
-
@type = type
|
|
40
|
-
@map = map || {}
|
|
41
|
-
@fallback = fallback
|
|
42
51
|
@requires = GraphWeaver::Codegen.normalize_requires!(requires, load: true)
|
|
43
52
|
|
|
44
53
|
if fallback && !type.values.include?(fallback)
|
|
@@ -46,28 +55,128 @@ class GraphWeaver::Codegen
|
|
|
46
55
|
end
|
|
47
56
|
end
|
|
48
57
|
|
|
49
|
-
# wire
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
# The wire tables for a mapped enum: [wire value => member, member => the
|
|
59
|
+
# wire value that goes out]. Every spelling casts; an alias's target is the
|
|
60
|
+
# one that serializes.
|
|
61
|
+
def tables_for(wire_values)
|
|
62
|
+
aliases = aliases_for(wire_values)
|
|
63
|
+
from_wire = {}
|
|
53
64
|
missing = []
|
|
54
65
|
|
|
55
66
|
wire_values.each do |wire|
|
|
56
|
-
|
|
57
|
-
member
|
|
67
|
+
canonical = aliases.fetch(wire, wire)
|
|
68
|
+
member = @map[canonical] || infer(canonical)
|
|
69
|
+
member ? from_wire[wire] = member : missing << canonical
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
if missing.any? && !fallback
|
|
61
73
|
raise GraphWeaver::Error,
|
|
62
|
-
"#{type} has no member for #{graphql_name} value(s) #{missing.join(", ")} — " \
|
|
74
|
+
"#{type} has no member for #{graphql_name} value(s) #{missing.uniq.join(", ")} — " \
|
|
63
75
|
"add them, pin with map:, or absorb with fallback:"
|
|
64
76
|
end
|
|
65
77
|
|
|
66
|
-
|
|
78
|
+
[from_wire, to_wire(from_wire, aliases)]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# alias spelling => the value it is read as, checked against what the
|
|
82
|
+
# schema declares — an alias for or onto a value that isn't there is a
|
|
83
|
+
# registration this schema disproves.
|
|
84
|
+
def aliases_for(wire_values)
|
|
85
|
+
declared = wire_values.sort.join(", ")
|
|
86
|
+
|
|
87
|
+
@aliases.each do |from, to|
|
|
88
|
+
unless wire_values.include?(from)
|
|
89
|
+
raise GraphWeaver::Error,
|
|
90
|
+
"enum #{graphql_name} has no value #{from.inspect} to alias — its values are #{declared}; " \
|
|
91
|
+
"fix the spelling or drop the alias"
|
|
92
|
+
end
|
|
93
|
+
unless wire_values.include?(to)
|
|
94
|
+
raise GraphWeaver::Error,
|
|
95
|
+
"enum #{graphql_name}: alias #{from.inspect} => #{to.inspect} names no value of #{graphql_name} — " \
|
|
96
|
+
"its values are #{declared}; the target is the spelling that goes on the wire"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@aliases
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The register_enum a set of indistinguishable wire values needs, ready to
|
|
104
|
+
# paste. SCREAMING_CASE is the GraphQL convention, so the odd spelling is
|
|
105
|
+
# guessed as the alias — every caller's sentence says to check the direction.
|
|
106
|
+
def self.alias_suggestion(graphql_name, groups, type = nil)
|
|
107
|
+
pairs = groups.flat_map { |group|
|
|
108
|
+
target = group.find { |value| value == value.upcase } || group.first
|
|
109
|
+
(group - [target]).map { |value| "#{value.inspect} => #{target.inspect}" }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
"GraphWeaver.register_enum(#{graphql_name.inspect}#{type ? ", #{type}" : ""}, " \
|
|
113
|
+
"alias: { #{pairs.join(", ")} })"
|
|
67
114
|
end
|
|
68
115
|
|
|
69
116
|
private
|
|
70
117
|
|
|
118
|
+
# Without a T::Enum there is nothing for map:/fallback:/requires: to
|
|
119
|
+
# describe, so alias: is the whole registration.
|
|
120
|
+
def alias_only!(map, fallback, requires)
|
|
121
|
+
if @aliases.empty?
|
|
122
|
+
raise ArgumentError, "register_enum(#{graphql_name.inspect}) says nothing about #{graphql_name} — " \
|
|
123
|
+
"pass the T::Enum to map it onto, or alias: { \"old\" => \"NEW\" } to read two wire values as one"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
extra = { map:, fallback:, requires: }.compact.keys.first
|
|
127
|
+
if extra
|
|
128
|
+
raise ArgumentError,
|
|
129
|
+
"register_enum(#{graphql_name.inspect}, alias: {...}) takes no #{extra}: — that describes a T::Enum " \
|
|
130
|
+
"of your own, so pass one: register_enum(#{graphql_name.inspect}, YourEnum, alias: {...})"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@requires = []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Sorted so generated source is stable across registration order.
|
|
137
|
+
def normalize_aliases!(aliases)
|
|
138
|
+
return {} if aliases.nil?
|
|
139
|
+
|
|
140
|
+
unless aliases.is_a?(Hash)
|
|
141
|
+
raise ArgumentError, "alias: is a hash of wire value => wire value, got #{aliases.inspect}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
pairs = aliases.to_h { |from, to| [from.to_s, to.to_s] }
|
|
145
|
+
|
|
146
|
+
self_alias = pairs.find { |from, to| from == to }
|
|
147
|
+
if self_alias
|
|
148
|
+
raise ArgumentError, "register_enum(#{graphql_name.inspect}): alias #{self_alias.first.inspect} => " \
|
|
149
|
+
"#{self_alias.last.inspect} reads a value as itself — drop it"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
chained = pairs.keys.find { |from| pairs.value?(from) }
|
|
153
|
+
if chained
|
|
154
|
+
raise ArgumentError, "register_enum(#{graphql_name.inspect}): #{chained.inspect} is both an alias and " \
|
|
155
|
+
"the value an alias points at — an alias can't chain; point every spelling at the one that goes on the wire"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
pairs.sort.to_h
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# member => the one wire value it serializes to. Two spellings on one
|
|
162
|
+
# member with no alias saying which goes out is ambiguous — invert used to
|
|
163
|
+
# pick whichever came last, which for a deprecation pair was the dead one.
|
|
164
|
+
def to_wire(from_wire, aliases)
|
|
165
|
+
canonical = from_wire.except(*aliases.keys)
|
|
166
|
+
ambiguous = canonical.group_by { |_, member| member }.select { |_, pairs| pairs.size > 1 }
|
|
167
|
+
if ambiguous.any?
|
|
168
|
+
member, = ambiguous.first
|
|
169
|
+
groups = ambiguous.values.map { |pairs| pairs.map(&:first) }
|
|
170
|
+
more = ambiguous.size - 1
|
|
171
|
+
raise GraphWeaver::Error,
|
|
172
|
+
"enum #{graphql_name}: #{groups.first.join(" and ")} both map onto the #{type} member " \
|
|
173
|
+
"#{member.serialize.to_s.inspect}#{" (and #{more} more)" unless more.zero?} — say which spelling " \
|
|
174
|
+
"goes on the wire:\n #{EnumType.alias_suggestion(graphql_name, groups, type)}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
canonical.invert
|
|
178
|
+
end
|
|
179
|
+
|
|
71
180
|
# "CAT" matches serialize "cat"; "NOT_FOUND" matches "not_found"
|
|
72
181
|
def infer(wire)
|
|
73
182
|
@type.values.find { |member| normalize(member.serialize.to_s) == normalize(wire) }
|
|
@@ -80,19 +189,23 @@ class GraphWeaver::Codegen
|
|
|
80
189
|
|
|
81
190
|
# The enum half of one graph's registrations — see Codegen::Registry.
|
|
82
191
|
class Registry
|
|
83
|
-
# Map a GraphQL enum onto an app-owned T::Enum
|
|
84
|
-
#
|
|
85
|
-
#
|
|
192
|
+
# Map a GraphQL enum onto an app-owned T::Enum, or fold two of its wire
|
|
193
|
+
# spellings into one value (see EnumType). The one implementation —
|
|
194
|
+
# GraphWeaver.register_enum is a delegate, so the same call reaches it
|
|
195
|
+
# whichever door you came in by.
|
|
86
196
|
#
|
|
87
197
|
# A value map is a natural third *positional* guess, and Ruby's arity
|
|
88
198
|
# complaint ("given 3, expected 2") never mentions the keyword.
|
|
89
|
-
def register_enum(graphql_name, type, positional_map = nil, map: nil, fallback: nil, requires: nil
|
|
199
|
+
def register_enum(graphql_name, type = nil, positional_map = nil, map: nil, fallback: nil, requires: nil,
|
|
200
|
+
alias: nil)
|
|
90
201
|
if positional_map
|
|
91
202
|
raise GraphWeaver::Error, "register_enum: the value map is a keyword — " \
|
|
92
203
|
"register_enum(#{graphql_name.inspect}, #{type}, map: {...})"
|
|
93
204
|
end
|
|
94
205
|
|
|
95
|
-
|
|
206
|
+
# `alias` is a Ruby keyword, so the parameter is only readable through binding
|
|
207
|
+
aliases = binding.local_variable_get(:alias)
|
|
208
|
+
enum_registry[graphql_name.to_s] = EnumType.new(graphql_name, type, map:, fallback:, requires:, aliases:)
|
|
96
209
|
end
|
|
97
210
|
|
|
98
211
|
def enum_registry
|
|
@@ -178,14 +178,16 @@ class GraphWeaver::Codegen
|
|
|
178
178
|
|
|
179
179
|
attr_reader :class_name, :fields
|
|
180
180
|
# the GraphQL type this struct was generated from, any registered helper
|
|
181
|
-
# modules to include,
|
|
182
|
-
|
|
181
|
+
# modules to include, any resolved alias delegators, and the member names a
|
|
182
|
+
# mixin declares abstract — Sorbet demands the override on those (extend_type)
|
|
183
|
+
attr_accessor :graphql_type, :mixins, :aliases, :overrides
|
|
183
184
|
|
|
184
185
|
def initialize(class_name)
|
|
185
186
|
@class_name = class_name
|
|
186
187
|
@fields = []
|
|
187
188
|
@mixins = []
|
|
188
189
|
@aliases = []
|
|
190
|
+
@overrides = []
|
|
189
191
|
end
|
|
190
192
|
|
|
191
193
|
def bare_type = class_name
|
|
@@ -203,17 +205,23 @@ class GraphWeaver::Codegen
|
|
|
203
205
|
end
|
|
204
206
|
|
|
205
207
|
class EnumNode < Node
|
|
206
|
-
attr_reader :class_name, :values
|
|
208
|
+
attr_reader :class_name, :values, :aliases
|
|
207
209
|
|
|
208
|
-
def initialize(class_name, values)
|
|
210
|
+
def initialize(class_name, values, aliases = {})
|
|
209
211
|
@class_name = class_name
|
|
210
212
|
@values = values
|
|
213
|
+
@aliases = aliases
|
|
211
214
|
end
|
|
212
215
|
|
|
213
216
|
def bare_type = class_name
|
|
214
217
|
|
|
218
|
+
# wire spellings this enum reads as another of its values (register_enum
|
|
219
|
+
# alias:) — a module-level table, emitted beside the class
|
|
220
|
+
def aliased? = !@aliases.empty?
|
|
221
|
+
def alias_const = "#{GraphWeaver::Inflect.underscore(class_name).upcase}_ALIASES"
|
|
222
|
+
|
|
215
223
|
def cast(expr, _depth)
|
|
216
|
-
"GraphWeaver::Hints.enum(#{class_name}, #{expr})"
|
|
224
|
+
"GraphWeaver::Hints.enum(#{class_name}, #{expr}#{", #{alias_const}" if aliased?})"
|
|
217
225
|
end
|
|
218
226
|
|
|
219
227
|
def serialize(expr, _depth)
|
|
@@ -227,7 +235,7 @@ class GraphWeaver::Codegen
|
|
|
227
235
|
def coerce? = true
|
|
228
236
|
|
|
229
237
|
def coerce(expr)
|
|
230
|
-
"GraphWeaver::InputStruct.enum(#{class_name}, #{expr})"
|
|
238
|
+
"GraphWeaver::InputStruct.enum(#{class_name}, #{expr}#{", #{alias_const}" if aliased?})"
|
|
231
239
|
end
|
|
232
240
|
|
|
233
241
|
def input_type = "T.any(#{class_name}, String)"
|
|
@@ -240,13 +248,13 @@ class GraphWeaver::Codegen
|
|
|
240
248
|
# <NAME>_TO_WIRE constants translate at the boundary. fallback: makes
|
|
241
249
|
# casting absorb unknown wire values (inputs stay strict).
|
|
242
250
|
class MappedEnum < Node
|
|
243
|
-
attr_reader :graphql_name, :mapping
|
|
251
|
+
attr_reader :graphql_name, :mapping, :to_wire
|
|
244
252
|
|
|
245
253
|
def initialize(enum_type, wire_values)
|
|
246
254
|
@graphql_name = enum_type.graphql_name
|
|
247
255
|
@type_name = enum_type.type.name
|
|
248
256
|
@fallback = enum_type.fallback
|
|
249
|
-
@mapping = enum_type.
|
|
257
|
+
@mapping, @to_wire = enum_type.tables_for(wire_values)
|
|
250
258
|
end
|
|
251
259
|
|
|
252
260
|
def const_prefix = GraphWeaver::Inflect.underscore(@graphql_name).upcase
|
|
@@ -379,7 +387,10 @@ class GraphWeaver::Codegen
|
|
|
379
387
|
# Joins the coerce protocol so execute kwargs accept plain hashes,
|
|
380
388
|
# normalized (and type-checked) through the generated .coerce.
|
|
381
389
|
class InputNode < Node
|
|
382
|
-
|
|
390
|
+
# type: the schema's spelling of the field's type ("[Float!]!"), which a
|
|
391
|
+
# refusal reports — the Sorbet prop type is the library's vocabulary, not
|
|
392
|
+
# the caller's.
|
|
393
|
+
Field = Struct.new(:prop, :wire, :node, :required, :type)
|
|
383
394
|
|
|
384
395
|
# graphql_name as well as class_name: a schema coordinate is spelled the
|
|
385
396
|
# schema's way (pokemon_bool_exp.name), which camelize has already lost.
|