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,325 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
class TypeRegistry
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
DEFAULT_TYPE_MAPPINGS = T.let(
|
|
10
|
+
{
|
|
11
|
+
"string" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
12
|
+
"integer" => RubyType.new(type: "::Integer", codec: "::OpenAPIKit::Codec::Integer"),
|
|
13
|
+
"number" => RubyType.new(type: "::Float", codec: "::OpenAPIKit::Codec::Float"),
|
|
14
|
+
"boolean" => RubyType.new(type: "T::Boolean", codec: "::OpenAPIKit::Codec::Boolean"),
|
|
15
|
+
|
|
16
|
+
"string:date-time" => RubyType.new(type: "::Time", codec: "::OpenAPIKit::Codec::DateTime"),
|
|
17
|
+
"string:date" => RubyType.new(type: "::Date", codec: "::OpenAPIKit::Codec::Date"),
|
|
18
|
+
"string:uuid" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::Uuid"),
|
|
19
|
+
"string:byte" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::Byte"),
|
|
20
|
+
"string:decimal" => RubyType.new(type: "::BigDecimal", codec: "::OpenAPIKit::Codec::Decimal"),
|
|
21
|
+
|
|
22
|
+
"string:time" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
23
|
+
"string:duration" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
24
|
+
"string:email" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
25
|
+
"string:idn-email" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
26
|
+
"string:hostname" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
27
|
+
"string:idn-hostname" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
28
|
+
"string:ipv4" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
29
|
+
"string:ipv6" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
30
|
+
"string:uri" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
31
|
+
"string:uri-reference" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
32
|
+
"string:uri-template" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
33
|
+
"string:iri" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
34
|
+
"string:json-pointer" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
35
|
+
"string:relative-json-pointer" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
36
|
+
"string:regex" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
37
|
+
"string:password" => RubyType.new(type: "::String", codec: "::OpenAPIKit::Codec::String"),
|
|
38
|
+
|
|
39
|
+
"integer:int32" => RubyType.new(type: "::Integer", codec: "::OpenAPIKit::Codec::Integer"),
|
|
40
|
+
"integer:int64" => RubyType.new(type: "::Integer", codec: "::OpenAPIKit::Codec::Integer"),
|
|
41
|
+
|
|
42
|
+
"number:float" => RubyType.new(type: "::Float", codec: "::OpenAPIKit::Codec::Float"),
|
|
43
|
+
"number:double" => RubyType.new(type: "::Float", codec: "::OpenAPIKit::Codec::Float"),
|
|
44
|
+
"number:decimal" => RubyType.new(type: "::BigDecimal", codec: "::OpenAPIKit::Codec::Decimal")
|
|
45
|
+
}.freeze,
|
|
46
|
+
T::Hash[String, RubyType]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
sig { params(document: Model::Document, config: Config).returns(TypeRegistry) }
|
|
50
|
+
def self.for(document, config)
|
|
51
|
+
reject_binary_mapping!(config.type_mappings)
|
|
52
|
+
|
|
53
|
+
registry = new(
|
|
54
|
+
namespace: config.namespace,
|
|
55
|
+
type_mappings: DEFAULT_TYPE_MAPPINGS.merge(config.type_mappings),
|
|
56
|
+
types: document.types.to_h { |type| [Model::TypeDef.name_of(type), type] }
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
registry.reject_alias_cycles!
|
|
60
|
+
registry
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(mappings: T::Hash[String, RubyType]).void }
|
|
64
|
+
def self.reject_binary_mapping!(mappings)
|
|
65
|
+
return unless mappings.key?("string:binary")
|
|
66
|
+
|
|
67
|
+
raise ConfigError,
|
|
68
|
+
"type_mappings has an entry for \"string:binary\", but openapi_kit does not convert a " \
|
|
69
|
+
"file with a codec: it has no wire form. An upload decodes to " \
|
|
70
|
+
"::ActionDispatch::Http::UploadedFile and a binary response body is a stream, " \
|
|
71
|
+
"neither through a codec. Convert to your own type in the handler, or use " \
|
|
72
|
+
"format: byte to carry bytes inside a value."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# An alias contributes no constant of its own: every reference to it expands to its
|
|
76
|
+
# target. A loop of them therefore has nothing to expand to, and would otherwise
|
|
77
|
+
# recur until the stack ran out.
|
|
78
|
+
sig { void }
|
|
79
|
+
def reject_alias_cycles!
|
|
80
|
+
@types.values.grep(Model::AliasDef).sort_by(&:name).each do |type|
|
|
81
|
+
walk_aliases(type.target, [type.name])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { returns(T::Array[String]) }
|
|
86
|
+
def warnings = @warnings.to_a
|
|
87
|
+
|
|
88
|
+
sig do
|
|
89
|
+
params(namespace: String, type_mappings: T::Hash[String, RubyType],
|
|
90
|
+
types: T::Hash[String, Model::TypeDef]).void
|
|
91
|
+
end
|
|
92
|
+
def initialize(namespace:, type_mappings: DEFAULT_TYPE_MAPPINGS, types: {})
|
|
93
|
+
@namespace = namespace
|
|
94
|
+
@type_mappings = type_mappings
|
|
95
|
+
@types = types
|
|
96
|
+
@warnings = T.let(Set.new, T::Set[String])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
UPLOADED_FILE = "::ActionDispatch::Http::UploadedFile"
|
|
100
|
+
|
|
101
|
+
BINARY = "::OpenAPIKit::Body::Binary"
|
|
102
|
+
|
|
103
|
+
sig { params(schema: Model::Schema, value: String).returns(T.nilable(String)) }
|
|
104
|
+
def from_form_expr(schema, value:)
|
|
105
|
+
return nil unless schema.is_a?(Model::Ref) && @types[schema.name].is_a?(Model::FormDef)
|
|
106
|
+
|
|
107
|
+
"#{@namespace}::Types::#{schema.name}::Form.from_parts(#{value})"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig { params(schema: Model::Schema).returns(String) }
|
|
111
|
+
def sorbet_type(schema)
|
|
112
|
+
case schema
|
|
113
|
+
when Model::Ref
|
|
114
|
+
aliased = alias_target(schema)
|
|
115
|
+
return sorbet_type(aliased) if aliased
|
|
116
|
+
|
|
117
|
+
union?(schema) ? "#{@namespace}::Types::#{schema.name}::Value" : "#{@namespace}::Types::#{schema.name}"
|
|
118
|
+
when Model::List then "T::Array[#{sorbet_type(schema.items)}]"
|
|
119
|
+
when Model::Freeform
|
|
120
|
+
values = schema.values
|
|
121
|
+
"T::Hash[::String, #{values ? sorbet_type(values) : "T.untyped"}]"
|
|
122
|
+
when Model::Untyped then "T.untyped"
|
|
123
|
+
when Model::StringSchema, Model::IntegerSchema, Model::NumberSchema, Model::BooleanSchema
|
|
124
|
+
ruby_type_for(schema).type
|
|
125
|
+
else T.absurd(schema)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
sig { params(schema: Model::Schema, value: String).returns(String) }
|
|
130
|
+
def from_wire_expr(schema, value:)
|
|
131
|
+
case schema
|
|
132
|
+
when Model::Ref
|
|
133
|
+
aliased = alias_target(schema)
|
|
134
|
+
aliased ? from_wire_expr(aliased, value: value) : "#{codec_for(schema)}.from_wire(#{value})"
|
|
135
|
+
when Model::List
|
|
136
|
+
"OpenAPIKit::Decode.each(#{value}) { |item| #{from_wire_expr(schema.items, value: "item")} }"
|
|
137
|
+
when Model::Freeform
|
|
138
|
+
values = schema.values
|
|
139
|
+
return "OpenAPIKit::Decode.object(#{value})" if values.nil?
|
|
140
|
+
|
|
141
|
+
"OpenAPIKit::Decode.values(#{value}) { |item| #{from_wire_expr(values, value: "item")} }"
|
|
142
|
+
when Model::Untyped then value
|
|
143
|
+
when Model::StringSchema, Model::IntegerSchema, Model::NumberSchema, Model::BooleanSchema
|
|
144
|
+
"#{ruby_type_for(schema).codec}.from_wire(#{value})"
|
|
145
|
+
else T.absurd(schema)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
NATIVE_LITERALS = T.let(
|
|
150
|
+
{
|
|
151
|
+
"::String" => [::String],
|
|
152
|
+
"::Integer" => [::Integer],
|
|
153
|
+
"::Float" => [::Float],
|
|
154
|
+
"T::Boolean" => [::TrueClass, ::FalseClass]
|
|
155
|
+
}.freeze,
|
|
156
|
+
T::Hash[String, T::Array[T::Class[T.anything]]]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
sig { params(schema: Model::Schema, value: T.untyped).returns(T::Boolean) }
|
|
160
|
+
def literal_matches_type?(schema, value)
|
|
161
|
+
NATIVE_LITERALS.fetch(sorbet_type(schema), []).any? { |native| value.is_a?(native) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
sig { params(name: String).returns(String) }
|
|
165
|
+
def codec_reference(name) = "#{@namespace}::Types::#{name}::Codec"
|
|
166
|
+
|
|
167
|
+
# Rails only hands back request_parameters verbatim for a JSON object, and wraps
|
|
168
|
+
# anything else under "_json", so the emitters need to know which shape a body
|
|
169
|
+
# will arrive in.
|
|
170
|
+
sig { params(schema: Model::Schema).returns(T::Boolean) }
|
|
171
|
+
def object?(schema)
|
|
172
|
+
case schema
|
|
173
|
+
when Model::Ref then referent_object?(schema)
|
|
174
|
+
when Model::Freeform, Model::Untyped then true
|
|
175
|
+
when Model::List, Model::StringSchema, Model::IntegerSchema, Model::NumberSchema,
|
|
176
|
+
Model::BooleanSchema
|
|
177
|
+
false
|
|
178
|
+
else T.absurd(schema)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
sig { params(schema: Model::Schema, value: String).returns(String) }
|
|
183
|
+
def to_wire_expr(schema, value:)
|
|
184
|
+
case schema
|
|
185
|
+
when Model::Ref
|
|
186
|
+
aliased = alias_target(schema)
|
|
187
|
+
aliased ? to_wire_expr(aliased, value: value) : "#{codec_for(schema)}.to_wire(#{value})"
|
|
188
|
+
when Model::List
|
|
189
|
+
inner = to_wire_expr(schema.items, value: "item")
|
|
190
|
+
inner == "item" ? value : "#{value}.map { |item| #{inner} }"
|
|
191
|
+
when Model::Freeform
|
|
192
|
+
values = schema.values
|
|
193
|
+
return value if values.nil?
|
|
194
|
+
|
|
195
|
+
inner = to_wire_expr(values, value: "item")
|
|
196
|
+
inner == "item" ? value : "#{value}.transform_values { |item| #{inner} }"
|
|
197
|
+
when Model::Untyped then value
|
|
198
|
+
when Model::StringSchema, Model::IntegerSchema, Model::NumberSchema, Model::BooleanSchema
|
|
199
|
+
"#{ruby_type_for(schema).codec}.to_wire(#{value})"
|
|
200
|
+
else T.absurd(schema)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
sig { params(schema: Model::Schema).void }
|
|
207
|
+
def reject_binary!(schema)
|
|
208
|
+
return unless Model::Schema.file?(schema)
|
|
209
|
+
|
|
210
|
+
raise SchemaError,
|
|
211
|
+
"format: binary has no JSON type, so it cannot be converted by a codec. It is " \
|
|
212
|
+
"only valid as a top-level property of a multipart/form-data request body, or " \
|
|
213
|
+
"as the whole schema of a response body."
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
sig { params(schema: T.nilable(Model::Schema), seen: T::Array[String]).void }
|
|
217
|
+
def walk_aliases(schema, seen)
|
|
218
|
+
case schema
|
|
219
|
+
when Model::Ref
|
|
220
|
+
found = @types[schema.name]
|
|
221
|
+
return unless found.is_a?(Model::AliasDef)
|
|
222
|
+
|
|
223
|
+
if seen.include?(schema.name)
|
|
224
|
+
raise SchemaError,
|
|
225
|
+
"#{schema.name} is defined in terms of itself, through " \
|
|
226
|
+
"#{seen.join(" -> ")} -> #{schema.name}. A schema that is only an alias for " \
|
|
227
|
+
"another cannot form a cycle: give one of them properties, or break the loop."
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
walk_aliases(found.target, seen + [schema.name])
|
|
231
|
+
when Model::List then walk_aliases(schema.items, seen)
|
|
232
|
+
when Model::Freeform then walk_aliases(schema.values, seen)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
sig { params(schema: Model::Ref).returns(String) }
|
|
237
|
+
def codec_for(schema) = codec_reference(schema.name)
|
|
238
|
+
|
|
239
|
+
sig { params(schema: Model::Ref).returns(T::Boolean) }
|
|
240
|
+
def union?(schema) = @types[schema.name].is_a?(Model::UnionDef)
|
|
241
|
+
|
|
242
|
+
sig { params(schema: Model::Ref).returns(T::Boolean) }
|
|
243
|
+
def referent_object?(schema)
|
|
244
|
+
found = @types[schema.name]
|
|
245
|
+
return true if found.nil?
|
|
246
|
+
|
|
247
|
+
case found
|
|
248
|
+
when Model::ObjectDef, Model::FormDef then true
|
|
249
|
+
when Model::EnumDef then false
|
|
250
|
+
when Model::UnionDef then found.members.all? { |member| object?(member) }
|
|
251
|
+
when Model::AliasDef then object?(found.target)
|
|
252
|
+
else T.absurd(found)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
sig { params(schema: Model::Ref).returns(T.nilable(Model::Schema)) }
|
|
257
|
+
def alias_target(schema)
|
|
258
|
+
found = @types[schema.name]
|
|
259
|
+
found.is_a?(Model::AliasDef) ? found.target : nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
sig { params(schema: Model::Schema).returns(RubyType) }
|
|
263
|
+
def ruby_type_for(schema)
|
|
264
|
+
override = Model::Schema.meta(schema).ruby_type
|
|
265
|
+
return override if override
|
|
266
|
+
|
|
267
|
+
reject_binary!(schema)
|
|
268
|
+
|
|
269
|
+
type, format = scalar_kind(schema)
|
|
270
|
+
requested = format ? "#{type}:#{format}" : type
|
|
271
|
+
|
|
272
|
+
candidate_keys(type, format).each_with_index do |key, index|
|
|
273
|
+
mapping = @type_mappings[key]
|
|
274
|
+
next if mapping.nil?
|
|
275
|
+
|
|
276
|
+
warn_about_unrecognised_format(requested, key) if index.positive?
|
|
277
|
+
return mapping
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
raise SchemaError, unmapped_message(requested)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
sig { params(type: String, format: T.nilable(String)).returns(T::Array[String]) }
|
|
284
|
+
def candidate_keys(type, format) = format ? ["#{type}:#{format}", type] : [type]
|
|
285
|
+
|
|
286
|
+
sig { params(schema: Model::Schema).returns([String, T.nilable(String)]) }
|
|
287
|
+
def scalar_kind(schema)
|
|
288
|
+
case schema
|
|
289
|
+
when Model::StringSchema then ["string", schema.format]
|
|
290
|
+
when Model::IntegerSchema then ["integer", schema.format]
|
|
291
|
+
when Model::NumberSchema then ["number", schema.format]
|
|
292
|
+
when Model::BooleanSchema then ["boolean", nil]
|
|
293
|
+
else raise SchemaError, "#{schema.class} is not a scalar"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
sig { params(requested: String, used: String).void }
|
|
298
|
+
def warn_about_unrecognised_format(requested, used)
|
|
299
|
+
fallback = T.must(@type_mappings[used])
|
|
300
|
+
message = <<~MESSAGE.strip
|
|
301
|
+
#{requested.inspect} has no Ruby type mapped, so it is treated as #{used.inspect}
|
|
302
|
+
(#{fallback.type}). If that is wrong, map it:
|
|
303
|
+
|
|
304
|
+
#{mapping_snippet(requested)}
|
|
305
|
+
MESSAGE
|
|
306
|
+
@warnings << message
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
sig { params(key: String).returns(String) }
|
|
310
|
+
def unmapped_message(key)
|
|
311
|
+
"No Ruby type is mapped for #{key.inspect}. Add one to your config:\n\n#{mapping_snippet(key)}"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
sig { params(key: String).returns(String) }
|
|
315
|
+
def mapping_snippet(key)
|
|
316
|
+
<<~SNIPPET.strip
|
|
317
|
+
type_mappings:
|
|
318
|
+
#{key.inspect}:
|
|
319
|
+
type: "::YourType"
|
|
320
|
+
codec: "YourApp::YourTypeCodec" # extends or includes OpenAPIKit::Codec
|
|
321
|
+
SNIPPET
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "prism"
|
|
7
|
+
|
|
8
|
+
module OpenAPIKit
|
|
9
|
+
module Codegen
|
|
10
|
+
class Writer
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(output: Pathname).void }
|
|
14
|
+
def initialize(output:)
|
|
15
|
+
@output = output
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { void }
|
|
19
|
+
def clean!
|
|
20
|
+
return unless output.directory?
|
|
21
|
+
|
|
22
|
+
existing = output.glob("**/*.rb").sort
|
|
23
|
+
foreign = existing.reject { |path| path.read.start_with?(MARKER) }
|
|
24
|
+
unless foreign.empty?
|
|
25
|
+
raise Error,
|
|
26
|
+
"Refusing to clean #{output}: #{foreign.size} file#{"s" if foreign.size > 1} " \
|
|
27
|
+
"not generated by openapi_kit " \
|
|
28
|
+
"(#{foreign.first(3).map { |f| f.relative_path_from(output) }.join(", ")}" \
|
|
29
|
+
"#{"…" if foreign.size > 3}). Point `output` at a directory openapi_kit owns."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
existing.each(&:delete)
|
|
33
|
+
prune_empty_directories
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(files: T::Array[Emit::SourceFile]).returns(T::Array[Pathname]) }
|
|
37
|
+
def write_all(files)
|
|
38
|
+
files.each { |file| verify_syntax!(file.path, file.contents) }
|
|
39
|
+
reject_duplicate_paths!(files)
|
|
40
|
+
|
|
41
|
+
clean!
|
|
42
|
+
files.map { |file| write(file.path, file.contents) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(name: String, content: String).returns(Pathname) }
|
|
46
|
+
def write(name, content)
|
|
47
|
+
verify_syntax!(name, content)
|
|
48
|
+
path = output.join(name)
|
|
49
|
+
FileUtils.mkdir_p(path.dirname)
|
|
50
|
+
body = content.end_with?("\n") ? content : "#{content}\n"
|
|
51
|
+
path.write("#{MARKER}\n#{body}")
|
|
52
|
+
path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
sig { returns(Pathname) }
|
|
58
|
+
attr_reader :output
|
|
59
|
+
|
|
60
|
+
sig { params(files: T::Array[Emit::SourceFile]).void }
|
|
61
|
+
def reject_duplicate_paths!(files)
|
|
62
|
+
duplicates = files.group_by(&:path).select { |_, group| group.size > 1 }
|
|
63
|
+
return if duplicates.empty?
|
|
64
|
+
|
|
65
|
+
raise Error,
|
|
66
|
+
"openapi_kit generated more than one file for #{duplicates.keys.sort.join(", ")}. " \
|
|
67
|
+
"Two names in your document normalise to the same constant. " \
|
|
68
|
+
"Disambiguate them, or set name_overrides in your config."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { void }
|
|
72
|
+
def prune_empty_directories
|
|
73
|
+
output.glob("**/*").select(&:directory?).sort.reverse_each do |directory|
|
|
74
|
+
directory.rmdir if directory.children.empty?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(name: String, content: String).void }
|
|
79
|
+
def verify_syntax!(name, content)
|
|
80
|
+
result = Prism.parse(content)
|
|
81
|
+
return if result.success?
|
|
82
|
+
|
|
83
|
+
error = T.must(result.errors.first)
|
|
84
|
+
line = error.location.start_line
|
|
85
|
+
raise Error,
|
|
86
|
+
"openapi_kit generated invalid Ruby in #{name} at line #{line}: #{error.message}\n " \
|
|
87
|
+
"#{content.lines[line - 1]&.strip}\n" \
|
|
88
|
+
"This is a bug in openapi_kit, not in your schema. Please report it."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "openapi_kit/runtime"
|
|
5
|
+
|
|
6
|
+
require "openapi_kit/codegen/naming"
|
|
7
|
+
require "openapi_kit/codegen/ruby_type"
|
|
8
|
+
require "openapi_kit/codegen/model/schema"
|
|
9
|
+
require "openapi_kit/codegen/model/type_def"
|
|
10
|
+
require "openapi_kit/codegen/model/document"
|
|
11
|
+
require "openapi_kit/codegen/type_registry"
|
|
12
|
+
require "openapi_kit/codegen/config"
|
|
13
|
+
require "openapi_kit/codegen/writer"
|
|
14
|
+
require "openapi_kit/codegen/loader"
|
|
15
|
+
require "openapi_kit/codegen/emit/buffer"
|
|
16
|
+
require "openapi_kit/codegen/emit/source_file"
|
|
17
|
+
require "openapi_kit/codegen/emit/emitter"
|
|
18
|
+
require "openapi_kit/codegen/emit/literal"
|
|
19
|
+
require "openapi_kit/codegen/emit/defaults"
|
|
20
|
+
require "openapi_kit/codegen/emit/decode"
|
|
21
|
+
require "openapi_kit/codegen/emit/types"
|
|
22
|
+
require "openapi_kit/codegen/emit/codecs"
|
|
23
|
+
require "openapi_kit/codegen/emit/forms"
|
|
24
|
+
require "openapi_kit/codegen/emit/operations"
|
|
25
|
+
require "openapi_kit/codegen/emit/handlers"
|
|
26
|
+
require "openapi_kit/codegen/emit/security"
|
|
27
|
+
require "openapi_kit/codegen/emit/registry"
|
|
28
|
+
require "openapi_kit/codegen/emit/routes"
|
|
29
|
+
require "openapi_kit/codegen/emit/controllers"
|
|
30
|
+
require "openapi_kit/codegen/generator"
|
|
31
|
+
require "openapi_kit/codegen/cli"
|
|
32
|
+
|
|
33
|
+
module OpenAPIKit
|
|
34
|
+
module Codegen
|
|
35
|
+
MARKER = "# Generated by openapi_kit. Do not edit. https://github.com/Nexus-Mods/openapi_kit"
|
|
36
|
+
end
|
|
37
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: openapi_kit-codegen
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jack Robertson
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: openapi3_parser
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.10.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.10.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: openapi_kit
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.1.0.pre.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.1.0.pre.1
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: prism
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
description: A spec-first code generator producing strict Sorbet interfaces bound
|
|
55
|
+
to implementations via dry-container, with sealed response types and an extensible
|
|
56
|
+
type registry.
|
|
57
|
+
email:
|
|
58
|
+
- jack.robertson@nexusmods.com
|
|
59
|
+
executables:
|
|
60
|
+
- openapi_kit
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- LICENSE.txt
|
|
65
|
+
- README.md
|
|
66
|
+
- exe/openapi_kit
|
|
67
|
+
- lib/openapi_kit-codegen.rb
|
|
68
|
+
- lib/openapi_kit/codegen.rb
|
|
69
|
+
- lib/openapi_kit/codegen/cli.rb
|
|
70
|
+
- lib/openapi_kit/codegen/config.rb
|
|
71
|
+
- lib/openapi_kit/codegen/emit/buffer.rb
|
|
72
|
+
- lib/openapi_kit/codegen/emit/codecs.rb
|
|
73
|
+
- lib/openapi_kit/codegen/emit/controllers.rb
|
|
74
|
+
- lib/openapi_kit/codegen/emit/decode.rb
|
|
75
|
+
- lib/openapi_kit/codegen/emit/defaults.rb
|
|
76
|
+
- lib/openapi_kit/codegen/emit/emitter.rb
|
|
77
|
+
- lib/openapi_kit/codegen/emit/forms.rb
|
|
78
|
+
- lib/openapi_kit/codegen/emit/handlers.rb
|
|
79
|
+
- lib/openapi_kit/codegen/emit/literal.rb
|
|
80
|
+
- lib/openapi_kit/codegen/emit/operations.rb
|
|
81
|
+
- lib/openapi_kit/codegen/emit/registry.rb
|
|
82
|
+
- lib/openapi_kit/codegen/emit/routes.rb
|
|
83
|
+
- lib/openapi_kit/codegen/emit/security.rb
|
|
84
|
+
- lib/openapi_kit/codegen/emit/source_file.rb
|
|
85
|
+
- lib/openapi_kit/codegen/emit/types.rb
|
|
86
|
+
- lib/openapi_kit/codegen/generator.rb
|
|
87
|
+
- lib/openapi_kit/codegen/loader.rb
|
|
88
|
+
- lib/openapi_kit/codegen/model/document.rb
|
|
89
|
+
- lib/openapi_kit/codegen/model/schema.rb
|
|
90
|
+
- lib/openapi_kit/codegen/model/type_def.rb
|
|
91
|
+
- lib/openapi_kit/codegen/naming.rb
|
|
92
|
+
- lib/openapi_kit/codegen/ruby_type.rb
|
|
93
|
+
- lib/openapi_kit/codegen/type_registry.rb
|
|
94
|
+
- lib/openapi_kit/codegen/writer.rb
|
|
95
|
+
homepage: https://github.com/Nexus-Mods/openapi_kit
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
rubygems_mfa_required: 'true'
|
|
100
|
+
source_code_uri: https://github.com/Nexus-Mods/openapi_kit
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.4'
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.6.9
|
|
116
|
+
specification_version: 4
|
|
117
|
+
summary: Generate Sorbet-typed Rails server stubs from an OpenAPI 3 document.
|
|
118
|
+
test_files: []
|