graph_weaver 0.7.3 → 0.7.5
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/README.md +1 -0
- data/docs/errors.md +5 -2
- data/docs/federation.md +3 -2
- data/docs/generated_modules.md +176 -22
- data/docs/getting_started.md +174 -14
- data/docs/i18n.md +4 -4
- data/docs/migrating.md +119 -0
- data/docs/scalars.md +161 -35
- data/docs/testing.md +24 -3
- data/docs/upgrading.md +51 -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/generators/graph_weaver/install_generator.rb +32 -3
- data/lib/graph_weaver/client.rb +23 -0
- data/lib/graph_weaver/codegen/aliases.rb +23 -2
- data/lib/graph_weaver/codegen/emit.rb +35 -16
- data/lib/graph_weaver/codegen/enum_type.rb +149 -19
- data/lib/graph_weaver/codegen/nodes.rb +72 -37
- 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 +259 -106
- data/lib/graph_weaver/coerce.rb +25 -6
- data/lib/graph_weaver/federation.rb +1 -6
- data/lib/graph_weaver/graph.rb +4 -1
- data/lib/graph_weaver/hints.rb +23 -5
- data/lib/graph_weaver/in_process.rb +1 -3
- data/lib/graph_weaver/input_struct.rb +31 -10
- data/lib/graph_weaver/internal/subgraphs.rb +1 -10
- data/lib/graph_weaver/internal/unused.rb +32 -7
- data/lib/graph_weaver/internal/values.rb +12 -4
- data/lib/graph_weaver/internal.rb +84 -0
- data/lib/graph_weaver/logging.rb +26 -29
- data/lib/graph_weaver/query_module.rb +20 -5
- data/lib/graph_weaver/railtie.rb +7 -2
- data/lib/graph_weaver/rspec.rb +0 -1
- data/lib/graph_weaver/schema_loader.rb +7 -8
- data/lib/graph_weaver/tasks.rb +60 -5
- data/lib/graph_weaver/testing/fake_client.rb +4 -10
- data/lib/graph_weaver/testing/router.rb +26 -25
- data/lib/graph_weaver/testing.rb +101 -1
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +80 -74
- metadata +3 -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:))
|
|
@@ -119,6 +119,28 @@ module GraphWeaver
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# The same thing the header and the Exclude above say, told to GitHub:
|
|
123
|
+
# linguist-generated collapses these files in a diff ("Load diff") and
|
|
124
|
+
# drops them from the language breakdown. Display only — they stay
|
|
125
|
+
# versioned, expandable, and `git diff` here is untouched.
|
|
126
|
+
#
|
|
127
|
+
# Written even when there is no .gitattributes, unlike .rubocop.yml: a
|
|
128
|
+
# .gitattributes turns no tooling on, so there is no app this surprises.
|
|
129
|
+
def mark_generated_for_github
|
|
130
|
+
body = File.read(gitattributes) if File.exist?(gitattributes)
|
|
131
|
+
# already marked — a re-run, or done by hand
|
|
132
|
+
globs = linguist_globs.reject { |glob| body&.include?(glob) }
|
|
133
|
+
return if globs.empty?
|
|
134
|
+
|
|
135
|
+
lines = globs.map { |glob| "#{glob} linguist-generated" }.join("\n")
|
|
136
|
+
marks = <<~TEXT + lines + "\n"
|
|
137
|
+
# Machine-written by `rake graph_weaver:generate` — GitHub collapses it in
|
|
138
|
+
# diffs and leaves it out of the language breakdown.
|
|
139
|
+
TEXT
|
|
140
|
+
|
|
141
|
+
body ? append_to_file(GITATTRIBUTES, "\n#{marks}") : create_file(GITATTRIBUTES, marks)
|
|
142
|
+
end
|
|
143
|
+
|
|
122
144
|
# The `graphql:` tags need this require, and it has to be somewhere
|
|
123
145
|
# rspec actually loads. A spec/support file is not: rspec-rails ships
|
|
124
146
|
# the spec/support glob commented out, so the require sat there doing
|
|
@@ -191,6 +213,7 @@ module GraphWeaver
|
|
|
191
213
|
private
|
|
192
214
|
|
|
193
215
|
RUBOCOP_CONFIG = ".rubocop.yml"
|
|
216
|
+
GITATTRIBUTES = ".gitattributes"
|
|
194
217
|
|
|
195
218
|
# rails_helper first: rspec-rails writes both, and only rails_helper
|
|
196
219
|
# has Rails booted by the time the require runs.
|
|
@@ -203,6 +226,8 @@ module GraphWeaver
|
|
|
203
226
|
|
|
204
227
|
def rubocop_config = File.join(GraphWeaver.root, RUBOCOP_CONFIG)
|
|
205
228
|
|
|
229
|
+
def gitattributes = File.join(GraphWeaver.root, GITATTRIBUTES)
|
|
230
|
+
|
|
206
231
|
# Parsed, not counted: a `---` can also be a line inside a block scalar.
|
|
207
232
|
# A file rubocop itself can't read is left to rubocop to complain about.
|
|
208
233
|
def yaml_documents(body)
|
|
@@ -213,9 +238,13 @@ module GraphWeaver
|
|
|
213
238
|
|
|
214
239
|
# Every graph's output directory, so a multi-schema app is covered by
|
|
215
240
|
# the same run — read off the graphs rather than restated here.
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
def generated_outputs = GraphWeaver.graphs.map(&:output).uniq
|
|
242
|
+
|
|
243
|
+
def generated_globs = generated_outputs.map { |dir| File.join(dir, "**/*") }
|
|
244
|
+
|
|
245
|
+
# gitattributes patterns are gitignore-style, where `dir/**` is
|
|
246
|
+
# everything beneath dir — spelled as git reads it, not as rubocop does.
|
|
247
|
+
def linguist_globs = generated_outputs.map { |dir| File.join(dir, "**") }
|
|
219
248
|
|
|
220
249
|
# This install run is the one moment the user is guaranteed to be
|
|
221
250
|
# reading, and a composed supergraph changes what the next steps are:
|
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,
|
|
@@ -119,6 +119,7 @@ class GraphWeaver::Codegen
|
|
|
119
119
|
nilable = true
|
|
120
120
|
else
|
|
121
121
|
obj = object_of(cur)
|
|
122
|
+
refuse_hoisted!(node, name, hoisted_of(cur), seg)
|
|
122
123
|
unless obj
|
|
123
124
|
hint = if list_of(cur)
|
|
124
125
|
" — use .first or .last to pick an element"
|
|
@@ -155,6 +156,19 @@ class GraphWeaver::Codegen
|
|
|
155
156
|
ObjectNode::Alias.new(name, expr, type)
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
# A hop landing on a hoisted shared fragment: the struct is another
|
|
160
|
+
# module's, so the path stops here. Which query hoists is a property of how
|
|
161
|
+
# that query spreads the fragment, so optional: still skips it.
|
|
162
|
+
def refuse_hoisted!(node, name, ref, seg)
|
|
163
|
+
return unless ref
|
|
164
|
+
|
|
165
|
+
raise GraphWeaver::Error,
|
|
166
|
+
"alias #{name.inspect} on #{node.graphql_type}: '#{seg}' is inside the shared fragment " \
|
|
167
|
+
"#{ref.class_name}, which hoists to #{@types_namespace}::#{ref.class_name} — a path can't " \
|
|
168
|
+
"read into it. Register the alias on #{ref.graphql_type}, or select a field beside the " \
|
|
169
|
+
"spread to keep the struct local"
|
|
170
|
+
end
|
|
171
|
+
|
|
158
172
|
# Separate "this query didn't select it" from "no query could": a segment
|
|
159
173
|
# the schema doesn't declare on the type is a mistake in the registration,
|
|
160
174
|
# so it raises even for an optional alias — which otherwise turns a typo
|
|
@@ -185,7 +199,7 @@ class GraphWeaver::Codegen
|
|
|
185
199
|
# The leaf's Sorbet type as referenced from the aliased struct. Generated
|
|
186
200
|
# nested constants (structs, enums, unions) must carry the container path,
|
|
187
201
|
# since the delegator's `sig` is emitted in an outer struct where a bare
|
|
188
|
-
# `Sub` wouldn't resolve; scalars, mapped enums, and hoisted
|
|
202
|
+
# `Sub` wouldn't resolve; scalars, mapped enums, and hoisted refs are
|
|
189
203
|
# already top-level. `containers` is the class-name chain to the leaf.
|
|
190
204
|
def qualified_alias_type(node, containers)
|
|
191
205
|
node = node.of if node.is_a?(NonNull)
|
|
@@ -203,10 +217,17 @@ class GraphWeaver::Codegen
|
|
|
203
217
|
# there from the shared enums module — either way, no container prefix
|
|
204
218
|
when EnumNode then node.class_name
|
|
205
219
|
when UnionNode then "#{prefix}#{node.bare_type}"
|
|
206
|
-
else node.bare_type # Scalar, MappedEnum,
|
|
220
|
+
else node.bare_type # Scalar, MappedEnum, a hoisted ref — already top-level
|
|
207
221
|
end
|
|
208
222
|
end
|
|
209
223
|
|
|
224
|
+
# the hoisted-fragment reference a node resolves to (through NON_NULL), or nil
|
|
225
|
+
def hoisted_of(node)
|
|
226
|
+
node = T.let(node, T.untyped)
|
|
227
|
+
node = node.of while node.is_a?(NonNull)
|
|
228
|
+
node if node.is_a?(HoistedRefNode)
|
|
229
|
+
end
|
|
230
|
+
|
|
210
231
|
# the List a node wraps (through NON_NULL), or nil
|
|
211
232
|
def list_of(node)
|
|
212
233
|
node = T.let(node, T.untyped)
|
|
@@ -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
|
|
@@ -130,8 +130,8 @@ class GraphWeaver::Codegen
|
|
|
130
130
|
# The shared types artifact as files: one file per type under types/, plus
|
|
131
131
|
# types.rb — the manifest that requires them in the order the runtime needs
|
|
132
132
|
# (see below). One rule for all three kinds, so a schema migration diffs
|
|
133
|
-
# exactly the types it touched whether they're inputs, enums or
|
|
134
|
-
def emit_types_files(
|
|
133
|
+
# exactly the types it touched whether they're inputs, enums or fragments.
|
|
134
|
+
def emit_types_files(fragments)
|
|
135
135
|
files = {}
|
|
136
136
|
# a mapped enum's constants are its wire tables, but the file is still
|
|
137
137
|
# named for the GraphQL enum — one type, one file, whichever it is
|
|
@@ -142,15 +142,15 @@ class GraphWeaver::Codegen
|
|
|
142
142
|
}
|
|
143
143
|
inputs, = ordered_inputs
|
|
144
144
|
structs = inputs.map { |input| type_file(files, input.class_name) { |out| emit_input(input, out, 1) } }
|
|
145
|
-
hoisted =
|
|
145
|
+
hoisted = fragments.map { |node| type_file(files, node.class_name) { |out| emit_nested(node, out, 1) } }
|
|
146
146
|
|
|
147
147
|
out = []
|
|
148
148
|
out << "# typed: strict"
|
|
149
149
|
out << "# frozen_string_literal: true"
|
|
150
150
|
out << ""
|
|
151
151
|
out << "# Generated by GraphWeaver #{GraphWeaver::VERSION} — do not edit. Shared types for this schema —"
|
|
152
|
-
out << "# input types, enums, and
|
|
153
|
-
out << "# per type; query modules alias what they use."
|
|
152
|
+
out << "# input types, enums, and the types hoisted out of shared fragments — one"
|
|
153
|
+
out << "# file per type; query modules alias what they use."
|
|
154
154
|
out << ""
|
|
155
155
|
requires = @requires.uniq.sort
|
|
156
156
|
if requires.any?
|
|
@@ -175,7 +175,7 @@ class GraphWeaver::Codegen
|
|
|
175
175
|
# both spell them bare, and a T::Enum can't be forward-declared the way
|
|
176
176
|
# an input struct can
|
|
177
177
|
if enums.any? && (structs.any? || hoisted.any?)
|
|
178
|
-
out << "# enums first — input structs and
|
|
178
|
+
out << "# enums first — input structs and hoisted fragments spell them bare"
|
|
179
179
|
end
|
|
180
180
|
(enums.sort + structs.sort + hoisted.sort).each do |file|
|
|
181
181
|
out << "require_relative #{file.delete_suffix(".rb").inspect}"
|
|
@@ -210,11 +210,11 @@ class GraphWeaver::Codegen
|
|
|
210
210
|
# assembled from the generator's walked state.
|
|
211
211
|
def emit_module(root, variables, representations = [], operation_name = nil)
|
|
212
212
|
# Every shared type this module names: its variable root inputs, the
|
|
213
|
-
# enums it reached, and the
|
|
214
|
-
#
|
|
213
|
+
# enums it reached, and the fragments it hoisted (aliased so <Name> and
|
|
214
|
+
# <Name>.from_h resolve to the shared module).
|
|
215
215
|
aliases = if @types_namespace
|
|
216
216
|
(shared_input_names(variables) + shared_enum_names +
|
|
217
|
-
@
|
|
217
|
+
@used_fragments.map { |name| camelize(name) }).uniq.sort
|
|
218
218
|
else
|
|
219
219
|
[]
|
|
220
220
|
end
|
|
@@ -338,11 +338,23 @@ class GraphWeaver::Codegen
|
|
|
338
338
|
node.values.each do |value|
|
|
339
339
|
out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
|
|
340
340
|
end
|
|
341
|
+
if node.fallback?
|
|
342
|
+
out << "#{pad} # every other wire value casts here (register_enum fallback: true)"
|
|
343
|
+
out << "#{pad} #{node.fallback} = new(#{GraphWeaver::Internal::ENUM_FALLBACK_WIRE.inspect})"
|
|
344
|
+
end
|
|
341
345
|
out << "#{pad} end"
|
|
342
346
|
out << "#{pad}end"
|
|
347
|
+
return unless node.aliased?
|
|
348
|
+
|
|
349
|
+
out << "#{pad}# wire spellings read as another value of #{node.class_name} (register_enum alias:)"
|
|
350
|
+
out << "#{pad}#{node.alias_const} = T.let({"
|
|
351
|
+
node.aliases.each { |from, to| out << "#{pad} #{from.inspect} => #{to.inspect}," }
|
|
352
|
+
out << "#{pad}}.freeze, T::Hash[String, String])"
|
|
343
353
|
end
|
|
344
354
|
|
|
345
|
-
# module-level wire translation tables for an app-mapped enum
|
|
355
|
+
# module-level wire translation tables for an app-mapped enum. TO_WIRE is
|
|
356
|
+
# written out rather than inverted from FROM_WIRE: an alias puts two
|
|
357
|
+
# spellings on one member, and only one of them goes back out.
|
|
346
358
|
def emit_mapped_enum(node, out, indent)
|
|
347
359
|
pad = " " * indent
|
|
348
360
|
type = node.bare_type
|
|
@@ -354,7 +366,11 @@ class GraphWeaver::Codegen
|
|
|
354
366
|
out << "#{pad} #{wire.inspect} => #{type}.deserialize(#{member.serialize.to_s.inspect}),"
|
|
355
367
|
end
|
|
356
368
|
out << "#{pad}}.freeze, T::Hash[String, #{type}])"
|
|
357
|
-
out << "#{pad}#{prefix}_TO_WIRE = T.let(
|
|
369
|
+
out << "#{pad}#{prefix}_TO_WIRE = T.let({"
|
|
370
|
+
node.to_wire.each do |member, wire|
|
|
371
|
+
out << "#{pad} #{type}.deserialize(#{member.serialize.to_s.inspect}) => #{wire.inspect},"
|
|
372
|
+
end
|
|
373
|
+
out << "#{pad}}.freeze, T::Hash[#{type}, String])"
|
|
358
374
|
end
|
|
359
375
|
|
|
360
376
|
def emit_object(node, out, indent)
|
|
@@ -383,7 +399,10 @@ class GraphWeaver::Codegen
|
|
|
383
399
|
node.fields.each do |field|
|
|
384
400
|
note = renamed_note(field.prop, field.key)
|
|
385
401
|
out << "#{pad} #{note}" if note
|
|
386
|
-
|
|
402
|
+
# a prop satisfying a mixin's abstract sig has to declare the override,
|
|
403
|
+
# and a `const` says it as a rule — there is no sig to put it in
|
|
404
|
+
override = node.overrides.include?(field.prop) ? ", override: true" : ""
|
|
405
|
+
out << "#{pad} const :#{field.prop}, #{field.node.prop_type}#{override}"
|
|
387
406
|
end
|
|
388
407
|
|
|
389
408
|
out << ""
|
|
@@ -418,7 +437,7 @@ class GraphWeaver::Codegen
|
|
|
418
437
|
# selected field onto the struct, next to the honest wire data
|
|
419
438
|
node.aliases.each do |a|
|
|
420
439
|
out << ""
|
|
421
|
-
out << "#{pad} sig { returns(#{a.type}) }"
|
|
440
|
+
out << "#{pad} sig { #{"override." if node.overrides.include?(a.name)}returns(#{a.type}) }"
|
|
422
441
|
out << "#{pad} def #{a.name} = #{a.expr}"
|
|
423
442
|
end
|
|
424
443
|
out << "#{pad}end"
|
|
@@ -677,13 +696,13 @@ class GraphWeaver::Codegen
|
|
|
677
696
|
out << "#{pad} const :#{field.prop}, #{type}#{default}"
|
|
678
697
|
end
|
|
679
698
|
out << ""
|
|
680
|
-
out << "#{pad} # (prop, wire, required, serializer, coercer, coordinate) per field"
|
|
699
|
+
out << "#{pad} # (prop, wire, required, serializer, coercer, coordinate, type) per field"
|
|
681
700
|
out << "#{pad} FIELDS = T.let(["
|
|
682
701
|
node.fields.each do |field|
|
|
683
702
|
serializer = field.node.serialize_identity? ? "nil" : "->(v) { #{field.node.serialize("v", 1)} }"
|
|
684
703
|
coercer = field.node.hash_coerce_identity? ? "nil" : "->(v) { #{field.node.hash_coerce("v", 1)} }"
|
|
685
704
|
coordinate = "#{node.graphql_name}.#{field.wire}"
|
|
686
|
-
out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}, #{coordinate.inspect}),"
|
|
705
|
+
out << "#{pad} GraphWeaver::InputStruct::Field.new(:#{field.prop}, #{field.wire.inspect}, #{field.required}, #{serializer}, #{coercer}, #{coordinate.inspect}, #{field.type.inspect}),"
|
|
687
706
|
end
|
|
688
707
|
out << "#{pad} ].freeze, T::Array[GraphWeaver::InputStruct::Field])"
|
|
689
708
|
# InputStruct reads it with const_get, which privacy doesn't block
|