openapi_kit-codegen 0.1.0.pre.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +385 -0
- data/exe/openapi_kit +6 -0
- data/lib/openapi_kit/codegen/cli.rb +72 -0
- data/lib/openapi_kit/codegen/config.rb +114 -0
- data/lib/openapi_kit/codegen/emit/buffer.rb +92 -0
- data/lib/openapi_kit/codegen/emit/codecs.rb +221 -0
- data/lib/openapi_kit/codegen/emit/controllers.rb +263 -0
- data/lib/openapi_kit/codegen/emit/decode.rb +43 -0
- data/lib/openapi_kit/codegen/emit/defaults.rb +42 -0
- data/lib/openapi_kit/codegen/emit/emitter.rb +17 -0
- data/lib/openapi_kit/codegen/emit/forms.rb +56 -0
- data/lib/openapi_kit/codegen/emit/handlers.rb +65 -0
- data/lib/openapi_kit/codegen/emit/literal.rb +28 -0
- data/lib/openapi_kit/codegen/emit/operations.rb +277 -0
- data/lib/openapi_kit/codegen/emit/registry.rb +101 -0
- data/lib/openapi_kit/codegen/emit/routes.rb +77 -0
- data/lib/openapi_kit/codegen/emit/security.rb +183 -0
- data/lib/openapi_kit/codegen/emit/source_file.rb +30 -0
- data/lib/openapi_kit/codegen/emit/types.rb +153 -0
- data/lib/openapi_kit/codegen/generator.rb +48 -0
- data/lib/openapi_kit/codegen/loader.rb +730 -0
- data/lib/openapi_kit/codegen/model/document.rb +302 -0
- data/lib/openapi_kit/codegen/model/schema.rb +110 -0
- data/lib/openapi_kit/codegen/model/type_def.rb +89 -0
- data/lib/openapi_kit/codegen/naming.rb +78 -0
- data/lib/openapi_kit/codegen/ruby_type.rb +48 -0
- data/lib/openapi_kit/codegen/type_registry.rb +325 -0
- data/lib/openapi_kit/codegen/writer.rb +92 -0
- data/lib/openapi_kit/codegen.rb +37 -0
- data/lib/openapi_kit-codegen.rb +4 -0
- metadata +118 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Handlers
|
|
8
|
+
extend T::Sig
|
|
9
|
+
include Emitter
|
|
10
|
+
|
|
11
|
+
sig { params(document: Model::Document, config: Config).void }
|
|
12
|
+
def initialize(document:, config:)
|
|
13
|
+
@document = document
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(tag: String).returns(String) }
|
|
18
|
+
def self.module_name(tag) = Naming.pascal(tag)
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T::Array[SourceFile]) }
|
|
21
|
+
def render
|
|
22
|
+
by_tag.map do |tag, operations|
|
|
23
|
+
Source.file(path: "#{@config.module_path}/handlers/#{Naming.snake(tag)}.rb",
|
|
24
|
+
modules: @config.modules + ["Handlers"]) do |buffer|
|
|
25
|
+
emit_interface(buffer, tag, operations)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Hash[String, T::Array[Model::Operation]]) }
|
|
33
|
+
def by_tag = @document.operations.group_by(&:tag)
|
|
34
|
+
|
|
35
|
+
sig { params(buffer: Buffer, tag: String, operations: T::Array[Model::Operation]).void }
|
|
36
|
+
def emit_interface(buffer, tag, operations)
|
|
37
|
+
buffer.nest("module #{Handlers.module_name(tag)}") do
|
|
38
|
+
buffer.line("extend T::Sig")
|
|
39
|
+
buffer.line("extend T::Helpers")
|
|
40
|
+
buffer.line("interface!")
|
|
41
|
+
|
|
42
|
+
operations.each do |operation|
|
|
43
|
+
buffer.blank
|
|
44
|
+
emit_method(buffer, operation)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
50
|
+
def emit_method(buffer, operation)
|
|
51
|
+
scope = "#{@config.namespace}::Operations::#{Emit::Operations.module_name(operation)}"
|
|
52
|
+
|
|
53
|
+
buffer.nest("sig do") do
|
|
54
|
+
buffer.line("abstract")
|
|
55
|
+
buffer.indent do
|
|
56
|
+
buffer.line(".params(request: #{scope}::Request)")
|
|
57
|
+
buffer.line(".returns(#{scope}::Response)")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
buffer.line("def #{Naming.identifier(operation.id)}(request:); end")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
module Literal
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
class Expr < T::Struct
|
|
11
|
+
const :code, String
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { params(code: String).returns(Expr) }
|
|
15
|
+
def self.expr(code) = Expr.new(code: code)
|
|
16
|
+
|
|
17
|
+
sig { params(parts: T.any(String, Expr)).returns(String) }
|
|
18
|
+
def self.string(*parts)
|
|
19
|
+
body = parts.map do |part|
|
|
20
|
+
part.is_a?(Expr) ? "\#{#{part.code}}" : T.must(part.dump[1..-2])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
%("#{body.join}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Operations
|
|
8
|
+
extend T::Sig
|
|
9
|
+
include Emitter
|
|
10
|
+
|
|
11
|
+
sig { params(document: Model::Document, registry: TypeRegistry, config: Config).void }
|
|
12
|
+
def initialize(document:, registry:, config:)
|
|
13
|
+
@document = document
|
|
14
|
+
@registry = registry
|
|
15
|
+
@config = config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { override.returns(T::Array[SourceFile]) }
|
|
19
|
+
def render
|
|
20
|
+
@document.operations.map do |operation|
|
|
21
|
+
Source.file(path: "#{@config.module_path}/operations/#{Naming.snake(operation.id)}.rb",
|
|
22
|
+
modules: @config.modules + ["Operations"]) do |buffer|
|
|
23
|
+
emit_operation(buffer, operation)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { params(operation: Model::Operation).returns(String) }
|
|
29
|
+
def self.module_name(operation) = Naming.pascal(operation.id)
|
|
30
|
+
|
|
31
|
+
GROUPS = T.let(
|
|
32
|
+
{
|
|
33
|
+
"Path" => Model::PathParameter,
|
|
34
|
+
"Query" => Model::QueryParameter,
|
|
35
|
+
"Headers" => Model::HeaderParameter,
|
|
36
|
+
"Cookies" => Model::CookieParameter
|
|
37
|
+
}.freeze,
|
|
38
|
+
T::Hash[String, T::Class[T.anything]]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
44
|
+
def emit_operation(buffer, operation)
|
|
45
|
+
buffer.nest("module #{Operations.module_name(operation)}") do
|
|
46
|
+
emit_parameter_structs(buffer, operation)
|
|
47
|
+
emit_request(buffer, operation)
|
|
48
|
+
buffer.blank
|
|
49
|
+
emit_response(buffer, operation)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(operation: Model::Operation).returns(T::Hash[String, T::Array[Model::Parameter]]) }
|
|
54
|
+
def groups(operation)
|
|
55
|
+
GROUPS.filter_map do |name, kind|
|
|
56
|
+
found = operation.parameters.grep(kind)
|
|
57
|
+
found.empty? ? nil : [name, found]
|
|
58
|
+
end.to_h
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
62
|
+
def emit_parameter_structs(buffer, operation)
|
|
63
|
+
groups(operation).each do |name, parameters|
|
|
64
|
+
buffer.nest("class #{name} < T::Struct") do
|
|
65
|
+
buffer.line("extend T::Sig")
|
|
66
|
+
buffer.blank
|
|
67
|
+
parameters.each { |parameter| buffer.line(parameter_prop(parameter)) }
|
|
68
|
+
end
|
|
69
|
+
buffer.blank
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(parameter: Model::Parameter).returns(String) }
|
|
74
|
+
def parameter_prop(parameter)
|
|
75
|
+
info = Model::Parameter.info(parameter)
|
|
76
|
+
meta = Model::Schema.meta(info.schema)
|
|
77
|
+
base = @registry.sorbet_type(info.schema)
|
|
78
|
+
default = meta.default
|
|
79
|
+
|
|
80
|
+
if default
|
|
81
|
+
clause = Defaults.clause(schema: info.schema, default: default, registry: @registry)
|
|
82
|
+
type = Defaults.nilable?(required: true, meta: meta) ? "T.nilable(#{base})" : base
|
|
83
|
+
return "const :#{info.identifier}, #{type}, #{clause}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return "const :#{info.identifier}, #{base}" if Model::Parameter.required?(parameter) && !meta.nullable
|
|
87
|
+
|
|
88
|
+
"const :#{info.identifier}, T.nilable(#{base})"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
92
|
+
def emit_request(buffer, operation)
|
|
93
|
+
buffer.nest("class Request < T::Struct") do
|
|
94
|
+
buffer.line("extend T::Sig")
|
|
95
|
+
buffer.blank
|
|
96
|
+
groups(operation).each_key { |name| buffer.line("const :#{Naming.identifier(name)}, #{name}") }
|
|
97
|
+
body = body_type(operation)
|
|
98
|
+
buffer.line("const :body, #{body}") if body
|
|
99
|
+
principal = principal_type(operation)
|
|
100
|
+
buffer.line("const :principal, #{principal}") if principal
|
|
101
|
+
buffer.line("const :http_request, ::ActionDispatch::Request")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Present only when `principals` is configured and the operation is protected:
|
|
106
|
+
# whatever the alternative that authenticated the request produced.
|
|
107
|
+
sig { params(operation: Model::Operation).returns(T.nilable(String)) }
|
|
108
|
+
def principal_type(operation)
|
|
109
|
+
return nil if @config.principal.nil?
|
|
110
|
+
|
|
111
|
+
requirements = @document.security_for(operation)
|
|
112
|
+
return nil if requirements.empty?
|
|
113
|
+
|
|
114
|
+
Security.principal_type(requirements, @config)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sig { params(operation: Model::Operation).returns(T.nilable(String)) }
|
|
118
|
+
def body_type(operation)
|
|
119
|
+
body = operation.request_body
|
|
120
|
+
return nil if body.nil?
|
|
121
|
+
|
|
122
|
+
content = single_content(body.contents, "the request body of #{operation.id}")
|
|
123
|
+
schema = content&.schema
|
|
124
|
+
return nil if schema.nil?
|
|
125
|
+
|
|
126
|
+
type = @registry.sorbet_type(schema)
|
|
127
|
+
body.required ? type : "T.nilable(#{type})"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { params(contents: T::Array[Model::Content], where: String).returns(T.nilable(Model::Content)) }
|
|
131
|
+
def single_content(contents, where)
|
|
132
|
+
return nil if contents.empty?
|
|
133
|
+
return contents.first if contents.one?
|
|
134
|
+
|
|
135
|
+
raise SchemaError,
|
|
136
|
+
"#{where} declares #{contents.size} content types " \
|
|
137
|
+
"(#{contents.map(&:media_type).join(", ")}). openapi_kit supports one content type per " \
|
|
138
|
+
"request body; split the alternatives into separate operations."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
142
|
+
def emit_response(buffer, operation)
|
|
143
|
+
variants = variants_for(operation)
|
|
144
|
+
|
|
145
|
+
buffer.nest("module Response") do
|
|
146
|
+
buffer.line("extend T::Helpers")
|
|
147
|
+
buffer.line("include ::OpenAPIKit::Response")
|
|
148
|
+
buffer.line("abstract!")
|
|
149
|
+
buffer.line("sealed!")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
variants.each do |variant|
|
|
153
|
+
buffer.blank
|
|
154
|
+
emit_variant(buffer, variant)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
module Payload
|
|
159
|
+
extend T::Helpers
|
|
160
|
+
abstract!
|
|
161
|
+
sealed!
|
|
162
|
+
|
|
163
|
+
class Empty < T::Struct
|
|
164
|
+
include Payload
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class Json < T::Struct
|
|
168
|
+
include Payload
|
|
169
|
+
|
|
170
|
+
const :schema, Model::Schema
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class Stream < T::Struct
|
|
174
|
+
include Payload
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class Variant < T::Struct
|
|
179
|
+
const :name, String
|
|
180
|
+
const :status, Model::Status
|
|
181
|
+
const :media_type, T.nilable(String)
|
|
182
|
+
const :payload, Payload
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
sig { params(operation: Model::Operation).returns(T::Array[Variant]) }
|
|
186
|
+
def variants_for(operation)
|
|
187
|
+
operation.responses.flat_map do |response|
|
|
188
|
+
base = Model::Status.constant(response.status)
|
|
189
|
+
next [empty_variant(base, response.status)] if response.contents.empty?
|
|
190
|
+
|
|
191
|
+
multiple = response.contents.size > 1
|
|
192
|
+
response.contents.map do |content|
|
|
193
|
+
suffix = multiple ? Naming.pascal(content.media_type.split("/").last.to_s.split("+").first.to_s) : ""
|
|
194
|
+
Variant.new(name: "#{base}#{suffix}", status: response.status,
|
|
195
|
+
media_type: content.media_type, payload: payload_for(content))
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
sig { params(name: String, status: Model::Status).returns(Variant) }
|
|
201
|
+
def empty_variant(name, status)
|
|
202
|
+
Variant.new(name: name, status: status, media_type: nil, payload: Payload::Empty.new)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
sig { params(content: Model::Content).returns(Payload) }
|
|
206
|
+
def payload_for(content)
|
|
207
|
+
schema = content.schema
|
|
208
|
+
return Payload::Empty.new if schema.nil?
|
|
209
|
+
return Payload::Stream.new if Model::Schema.file?(schema)
|
|
210
|
+
|
|
211
|
+
Payload::Json.new(schema: schema)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
sig { params(buffer: Buffer, variant: Variant).void }
|
|
215
|
+
def emit_variant(buffer, variant)
|
|
216
|
+
buffer.nest("class #{variant.name} < T::Struct") do
|
|
217
|
+
buffer.line("extend T::Sig")
|
|
218
|
+
buffer.line("include Response")
|
|
219
|
+
buffer.blank
|
|
220
|
+
|
|
221
|
+
props = variant_props(variant)
|
|
222
|
+
props.each { |prop| buffer.line(prop) }
|
|
223
|
+
buffer.blank unless props.empty?
|
|
224
|
+
|
|
225
|
+
buffer.line("sig { override.returns(::Integer) }")
|
|
226
|
+
buffer.line(status_method(variant.status))
|
|
227
|
+
buffer.blank
|
|
228
|
+
buffer.line("sig { override.returns(::OpenAPIKit::Body) }")
|
|
229
|
+
buffer.line("def to_body = #{to_body(variant.payload)}")
|
|
230
|
+
buffer.blank
|
|
231
|
+
buffer.line("sig { override.returns(T.nilable(::String)) }")
|
|
232
|
+
buffer.line("def content_type = #{variant.media_type.inspect}")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
sig { params(variant: Variant).returns(T::Array[String]) }
|
|
237
|
+
def variant_props(variant)
|
|
238
|
+
props = payload_props(variant.payload)
|
|
239
|
+
return props unless variant.status.is_a?(Model::DefaultStatus)
|
|
240
|
+
|
|
241
|
+
props + ["const :status_code, ::Integer"]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
sig { params(payload: Payload).returns(T::Array[String]) }
|
|
245
|
+
def payload_props(payload)
|
|
246
|
+
case payload
|
|
247
|
+
when Payload::Empty then []
|
|
248
|
+
when Payload::Json then ["const :body, #{@registry.sorbet_type(payload.schema)}"]
|
|
249
|
+
when Payload::Stream then ["const :body, #{TypeRegistry::BINARY}"]
|
|
250
|
+
else T.absurd(payload)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
sig { params(payload: Payload).returns(String) }
|
|
255
|
+
def to_body(payload)
|
|
256
|
+
case payload
|
|
257
|
+
when Payload::Empty then "::OpenAPIKit::Body::Empty.new"
|
|
258
|
+
when Payload::Json
|
|
259
|
+
"::OpenAPIKit::Body::Json.new(wire: #{@registry.to_wire_expr(payload.schema, value: "body")})"
|
|
260
|
+
when Payload::Stream then "body"
|
|
261
|
+
else T.absurd(payload)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
sig { params(status: Model::Status).returns(String) }
|
|
266
|
+
def status_method(status)
|
|
267
|
+
case status
|
|
268
|
+
when Model::StatusCode then "def status = #{status.code}"
|
|
269
|
+
when Model::StatusRange then "def status = #{status.hundreds * 100}"
|
|
270
|
+
when Model::DefaultStatus then "def status = status_code"
|
|
271
|
+
else T.absurd(status)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
# The typed boundary between openapi_kit's interfaces and an application's objects. Rails
|
|
8
|
+
# instantiates controllers itself, so a controller cannot be handed the registry: it
|
|
9
|
+
# reads it from the accessor, which an application assigns at boot.
|
|
10
|
+
class Registry
|
|
11
|
+
extend T::Sig
|
|
12
|
+
include Emitter
|
|
13
|
+
|
|
14
|
+
# One entry per thing an application must supply.
|
|
15
|
+
class Slot < T::Struct
|
|
16
|
+
const :reader, String
|
|
17
|
+
const :interface, String
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(document: Model::Document, config: Config).void }
|
|
21
|
+
def initialize(document:, config:)
|
|
22
|
+
@document = document
|
|
23
|
+
@config = config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.returns(T::Array[SourceFile]) }
|
|
27
|
+
def render
|
|
28
|
+
return [] if slots.empty?
|
|
29
|
+
|
|
30
|
+
[
|
|
31
|
+
Source.file(path: "#{@config.module_path}/registry.rb",
|
|
32
|
+
modules: @config.modules) { |buffer| emit_registry(buffer) },
|
|
33
|
+
Source.file(path: "#{@config.module_path}.rb",
|
|
34
|
+
modules: @config.modules) { |buffer| emit_accessor(buffer) }
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
sig { returns(T::Array[Slot]) }
|
|
41
|
+
def slots
|
|
42
|
+
handlers = @document.tags.map do |tag|
|
|
43
|
+
Slot.new(reader: Naming.snake(tag),
|
|
44
|
+
interface: "#{@config.namespace}::Handlers::#{Emit::Handlers.module_name(tag)}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
handlers + authenticated.map do |name|
|
|
48
|
+
Slot.new(reader: Naming.snake(name),
|
|
49
|
+
interface: "#{@config.namespace}::Security::#{Emit::Security.module_name(name)}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { returns(T::Array[String]) }
|
|
54
|
+
def authenticated
|
|
55
|
+
return [] if @config.principal.nil?
|
|
56
|
+
|
|
57
|
+
@document.operations.flat_map do |operation|
|
|
58
|
+
@document.security_for(operation).flat_map { |requirement| requirement.schemes.keys }
|
|
59
|
+
end.uniq
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(buffer: Buffer).void }
|
|
63
|
+
def emit_registry(buffer)
|
|
64
|
+
buffer.nest("class Registry < T::Struct") do
|
|
65
|
+
slots.each { |slot| buffer.line("const :#{slot.reader}, #{slot.interface}") }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Rails instantiates controllers itself, so a controller cannot be handed the
|
|
70
|
+
# registry. It reads it from here, and an application assigns it at boot.
|
|
71
|
+
sig { params(buffer: Buffer).void }
|
|
72
|
+
def emit_accessor(buffer)
|
|
73
|
+
buffer.line("extend T::Sig")
|
|
74
|
+
buffer.blank
|
|
75
|
+
buffer.line("@registry = T.let(nil, T.nilable(Registry))")
|
|
76
|
+
buffer.blank
|
|
77
|
+
buffer.line("sig { params(registry: Registry).void }")
|
|
78
|
+
buffer.nest("def self.registry=(registry)") do
|
|
79
|
+
buffer.line("@registry = registry")
|
|
80
|
+
end
|
|
81
|
+
buffer.blank
|
|
82
|
+
buffer.line("sig { returns(Registry) }")
|
|
83
|
+
buffer.nest("def self.registry") do
|
|
84
|
+
buffer.line("@registry || raise(")
|
|
85
|
+
buffer.indent { unset_message.each { |line| buffer.line(line) } }
|
|
86
|
+
buffer.line(")")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { returns(T::Array[String]) }
|
|
91
|
+
def unset_message
|
|
92
|
+
namespace = @config.namespace
|
|
93
|
+
[
|
|
94
|
+
%("#{namespace}.registry has not been assigned. Build one in an initializer, " \\),
|
|
95
|
+
%("e.g. #{namespace}.registry = #{namespace}::Registry.new(...).")
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Routes
|
|
8
|
+
extend T::Sig
|
|
9
|
+
include Emitter
|
|
10
|
+
|
|
11
|
+
sig { params(document: Model::Document, config: Config).void }
|
|
12
|
+
def initialize(document:, config:)
|
|
13
|
+
@document = document
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { override.returns(T::Array[SourceFile]) }
|
|
18
|
+
def render
|
|
19
|
+
return [] if @document.operations.empty?
|
|
20
|
+
|
|
21
|
+
[
|
|
22
|
+
Source.file(path: "#{@config.module_path}/routes.rb",
|
|
23
|
+
modules: @config.modules + ["Routes"]) { |buffer| emit_body(buffer) }
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
sig { params(buffer: Buffer).void }
|
|
30
|
+
def emit_body(buffer)
|
|
31
|
+
buffer.line("extend T::Sig")
|
|
32
|
+
buffer.blank
|
|
33
|
+
buffer.line("sig { params(mapper: ::ActionDispatch::Routing::Mapper).void }")
|
|
34
|
+
buffer.nest("def self.draw(mapper)") do
|
|
35
|
+
ordered(@document.operations).each { |operation| buffer.line(route_for(operation)) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Rails matches routes in declaration order, so a templated segment declared first
|
|
40
|
+
# swallows a concrete one: /mods/{id} would answer GET /mods/featured. OpenAPI paths
|
|
41
|
+
# are unordered, so the emitter imposes concrete-before-templated per segment.
|
|
42
|
+
sig { params(operations: T::Array[Model::Operation]).returns(T::Array[Model::Operation]) }
|
|
43
|
+
def ordered(operations)
|
|
44
|
+
operations.each_with_index.sort_by { |operation, index| [template_flags(operation), index] }
|
|
45
|
+
.map(&:first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(operation: Model::Operation).returns(T::Array[Integer]) }
|
|
49
|
+
def template_flags(operation)
|
|
50
|
+
operation.path.split("/").reject(&:empty?).map { |segment| segment.start_with?("{") ? 1 : 0 }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(operation: Model::Operation).returns(String) }
|
|
54
|
+
def route_for(operation)
|
|
55
|
+
verb = operation.http_method.serialize
|
|
56
|
+
controller = "#{@config.module_path}/controllers/#{Naming.snake(operation.tag)}"
|
|
57
|
+
target = "#{controller}##{Naming.identifier(operation.id)}"
|
|
58
|
+
|
|
59
|
+
"mapper.#{verb}(#{path_for(operation).inspect}, to: #{target.inspect}, format: false)"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(operation: Model::Operation).returns(String) }
|
|
63
|
+
def path_for(operation)
|
|
64
|
+
operation.path.scan(/\{([^}]*)\}/).flatten.each do |name|
|
|
65
|
+
next if name.match?(/\A\w+\z/)
|
|
66
|
+
|
|
67
|
+
raise SchemaError,
|
|
68
|
+
"#{operation.path} has the path template {#{name}}, which Rails cannot route. " \
|
|
69
|
+
"A path parameter name may contain only letters, digits and underscores."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
operation.path.gsub(/\{(\w+)}/, ':\1')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|