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,221 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Codecs
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { params(document: Model::Document, registry: TypeRegistry, config: Config).void }
|
|
11
|
+
def initialize(document:, registry:, config:)
|
|
12
|
+
@document = document
|
|
13
|
+
@registry = registry
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Codecable = T.type_alias do
|
|
18
|
+
T.any(Model::ObjectDef, Model::EnumDef, Model::UnionDef, Model::AliasDef)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { params(buffer: Buffer, type: Codecable).void }
|
|
22
|
+
def emit(buffer, type) = emit_codec(buffer, type)
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
sig { params(buffer: Buffer, type: Codecable).void }
|
|
27
|
+
def emit_codec(buffer, type)
|
|
28
|
+
name = Model::TypeDef.name_of(type)
|
|
29
|
+
buffer.nest("module Codec") do
|
|
30
|
+
buffer.line("extend T::Sig")
|
|
31
|
+
buffer.line("extend T::Generic")
|
|
32
|
+
buffer.line("extend ::OpenAPIKit::Codec::Contract")
|
|
33
|
+
buffer.blank
|
|
34
|
+
buffer.line("Value = type_template { { fixed: #{qualified(name)} } }")
|
|
35
|
+
buffer.blank
|
|
36
|
+
emit_codec_constants(buffer, type)
|
|
37
|
+
buffer.line("sig { override.params(value: ::OpenAPIKit::Wire).returns(#{qualified(name)}) }")
|
|
38
|
+
emit_from_wire(buffer, type)
|
|
39
|
+
buffer.blank
|
|
40
|
+
buffer.line("sig { override.params(value: #{qualified(name)}).returns(::OpenAPIKit::Wire) }")
|
|
41
|
+
emit_to_wire(buffer, type)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(buffer: Buffer, type: Codecable).void }
|
|
46
|
+
def emit_codec_constants(buffer, type)
|
|
47
|
+
tag = type.is_a?(Model::UnionDef) ? type.tag : nil
|
|
48
|
+
return unless tag.is_a?(Model::Tagged)
|
|
49
|
+
|
|
50
|
+
tags = tag.mapping.keys.map(&:inspect).join(", ")
|
|
51
|
+
buffer.line("TAGS = T.let([#{tags}].freeze, T::Array[::String])")
|
|
52
|
+
buffer.blank
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(name: String).returns(String) }
|
|
56
|
+
def qualified(name) = @registry.sorbet_type(Model::Ref.new(name: name))
|
|
57
|
+
|
|
58
|
+
sig { params(buffer: Buffer, type: Codecable).void }
|
|
59
|
+
def emit_from_wire(buffer, type)
|
|
60
|
+
case type
|
|
61
|
+
when Model::EnumDef then emit_enum_from_wire(buffer, type)
|
|
62
|
+
when Model::ObjectDef then emit_object_from_wire(buffer, type)
|
|
63
|
+
when Model::UnionDef then emit_union_from_wire(buffer, type)
|
|
64
|
+
when Model::AliasDef
|
|
65
|
+
buffer.line("def self.from_wire(value) = #{@registry.from_wire_expr(type.target, value: "value")}")
|
|
66
|
+
else T.absurd(type)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { params(buffer: Buffer, type: Codecable).void }
|
|
71
|
+
def emit_to_wire(buffer, type)
|
|
72
|
+
case type
|
|
73
|
+
when Model::EnumDef then buffer.line("def self.to_wire(value) = value.serialize")
|
|
74
|
+
when Model::ObjectDef then emit_object_to_wire(buffer, type)
|
|
75
|
+
when Model::UnionDef then emit_union_to_wire(buffer, type)
|
|
76
|
+
when Model::AliasDef
|
|
77
|
+
buffer.line("def self.to_wire(value) = #{@registry.to_wire_expr(type.target, value: "value")}")
|
|
78
|
+
else T.absurd(type)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(buffer: Buffer, type: Model::EnumDef).void }
|
|
83
|
+
def emit_enum_from_wire(buffer, type)
|
|
84
|
+
buffer.line("def self.from_wire(value) = ::OpenAPIKit::Decode.enum(#{qualified(type.name)}, value)")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { params(buffer: Buffer, type: Model::ObjectDef).void }
|
|
88
|
+
def emit_object_from_wire(buffer, type)
|
|
89
|
+
buffer.nest("def self.from_wire(value)") do
|
|
90
|
+
buffer.line("raw = ::OpenAPIKit::Decode.object(value)")
|
|
91
|
+
buffer.line("#{qualified(type.name)}.new(")
|
|
92
|
+
buffer.indent do
|
|
93
|
+
type.properties.each do |property|
|
|
94
|
+
buffer.line("#{property.identifier}: #{decode_from(property, source: "raw")},")
|
|
95
|
+
end
|
|
96
|
+
extra = type.additional_properties
|
|
97
|
+
buffer.line("additional_properties: #{additional_decode(type, extra)},") if extra
|
|
98
|
+
end
|
|
99
|
+
buffer.line(")")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { params(type: Model::ObjectDef, schema: Model::Schema).returns(String) }
|
|
104
|
+
def additional_decode(type, schema)
|
|
105
|
+
known = type.properties.map { |property| property.name.inspect }.join(", ")
|
|
106
|
+
inner = @registry.from_wire_expr(schema, value: "item")
|
|
107
|
+
"::OpenAPIKit::Decode.values(raw.except(#{known})) { |item| #{inner} }"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig { params(schema: Model::Schema).returns(String) }
|
|
111
|
+
def additional_encode(schema)
|
|
112
|
+
inner = @registry.to_wire_expr(schema, value: "item")
|
|
113
|
+
return "value.additional_properties" if inner == "item"
|
|
114
|
+
|
|
115
|
+
"value.additional_properties.transform_values { |item| #{inner} }"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { params(property: Model::Property, source: String).returns(String) }
|
|
119
|
+
def decode_from(property, source:)
|
|
120
|
+
Decode.expression(source: source, key: property.name, schema: property.schema,
|
|
121
|
+
required: property.required, registry: @registry)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
sig { params(buffer: Buffer, type: Model::ObjectDef).void }
|
|
125
|
+
def emit_object_to_wire(buffer, type)
|
|
126
|
+
buffer.nest("def self.to_wire(value)") do
|
|
127
|
+
buffer.line("wire = T.let({}, T::Hash[::String, ::OpenAPIKit::Wire])")
|
|
128
|
+
type.properties.each { |property| emit_property_to_wire(buffer, property) }
|
|
129
|
+
extra = type.additional_properties
|
|
130
|
+
buffer.line("wire.merge!(#{additional_encode(extra)})") if extra
|
|
131
|
+
buffer.line("wire")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { params(buffer: Buffer, property: Model::Property).void }
|
|
136
|
+
def emit_property_to_wire(buffer, property)
|
|
137
|
+
meta = Model::Schema.meta(property.schema)
|
|
138
|
+
key = property.name.inspect
|
|
139
|
+
reader = property.identifier.to_s
|
|
140
|
+
|
|
141
|
+
if Decode.optional_nullable?(required: property.required, meta: Model::Schema.meta(property.schema))
|
|
142
|
+
buffer.line("#{reader} = value.#{reader}")
|
|
143
|
+
buffer.nest("if #{reader}.is_a?(::OpenAPIKit::Present)") do
|
|
144
|
+
buffer.line("inner = #{reader}.value")
|
|
145
|
+
buffer.line("wire[#{key}] = inner.nil? ? nil : #{@registry.to_wire_expr(property.schema,
|
|
146
|
+
value: "inner")}")
|
|
147
|
+
end
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if property.required && !meta.nullable
|
|
152
|
+
buffer.line("wire[#{key}] = #{@registry.to_wire_expr(property.schema, value: "value.#{reader}")}")
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
buffer.line("#{reader} = value.#{reader}")
|
|
157
|
+
dumped = @registry.to_wire_expr(property.schema, value: reader)
|
|
158
|
+
|
|
159
|
+
if property.required
|
|
160
|
+
buffer.line("wire[#{key}] = #{reader}.nil? ? nil : #{dumped}")
|
|
161
|
+
else
|
|
162
|
+
buffer.line("wire[#{key}] = #{dumped} unless #{reader}.nil?")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sig { params(buffer: Buffer, type: Model::UnionDef).void }
|
|
167
|
+
def emit_union_from_wire(buffer, type)
|
|
168
|
+
tag = type.tag
|
|
169
|
+
buffer.nest("def self.from_wire(value)") do
|
|
170
|
+
case tag
|
|
171
|
+
when Model::Tagged then emit_tagged_from_wire(buffer, tag)
|
|
172
|
+
when Model::Untagged then emit_untagged_from_wire(buffer, type)
|
|
173
|
+
else T.absurd(tag)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
sig { params(buffer: Buffer, tag: Model::Tagged).void }
|
|
179
|
+
def emit_tagged_from_wire(buffer, tag)
|
|
180
|
+
buffer.line("raw = ::OpenAPIKit::Decode.object(value)")
|
|
181
|
+
buffer.line("tag = raw[#{tag.property_name.inspect}]")
|
|
182
|
+
buffer.case_of("tag") do
|
|
183
|
+
tag.mapping.each do |wire_value, name|
|
|
184
|
+
buffer.line("when #{wire_value.inspect} then #{codec_for(name)}.from_wire(raw)")
|
|
185
|
+
end
|
|
186
|
+
message = Literal.string("expected #{tag.property_name} to be one of ",
|
|
187
|
+
Literal.expr(%(TAGS.join(", "))), ", got ", Literal.expr("tag.inspect"))
|
|
188
|
+
buffer.line("else raise(::OpenAPIKit::DecodeError.new(#{message}))")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
sig { params(buffer: Buffer, type: Model::UnionDef).void }
|
|
193
|
+
def emit_untagged_from_wire(buffer, type)
|
|
194
|
+
buffer.line("::OpenAPIKit::Decode.first_of(value, #{type.name.inspect}, [")
|
|
195
|
+
buffer.indent do
|
|
196
|
+
type.members.each do |member|
|
|
197
|
+
buffer.line("->(candidate) { #{@registry.from_wire_expr(member, value: "candidate")} },")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
buffer.line("])")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
sig { params(buffer: Buffer, type: Model::UnionDef).void }
|
|
204
|
+
def emit_union_to_wire(buffer, type)
|
|
205
|
+
buffer.nest("def self.to_wire(value)") do
|
|
206
|
+
buffer.case_of("value") do
|
|
207
|
+
type.members.each do |member|
|
|
208
|
+
buffer.line("when #{@registry.sorbet_type(member)} then " \
|
|
209
|
+
"#{@registry.to_wire_expr(member, value: "value")}")
|
|
210
|
+
end
|
|
211
|
+
buffer.line("else T.absurd(value)")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
sig { params(name: String).returns(String) }
|
|
217
|
+
def codec_for(name) = @registry.codec_reference(name)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Controllers
|
|
8
|
+
extend T::Sig
|
|
9
|
+
include Emitter
|
|
10
|
+
|
|
11
|
+
SOURCES = T.let(
|
|
12
|
+
{
|
|
13
|
+
"Path" => "path_params",
|
|
14
|
+
"Query" => "query_params",
|
|
15
|
+
"Headers" => "header_params",
|
|
16
|
+
"Cookies" => "cookie_params"
|
|
17
|
+
}.freeze,
|
|
18
|
+
T::Hash[String, String]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
sig { params(document: Model::Document, registry: TypeRegistry, config: Config).void }
|
|
22
|
+
def initialize(document:, registry:, config:)
|
|
23
|
+
@document = document
|
|
24
|
+
@registry = registry
|
|
25
|
+
@config = config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { override.returns(T::Array[SourceFile]) }
|
|
29
|
+
def render
|
|
30
|
+
@document.operations.group_by(&:tag).map do |tag, operations|
|
|
31
|
+
Source.file(path: "#{@config.module_path}/controllers/#{Naming.snake(tag)}_controller.rb",
|
|
32
|
+
modules: @config.modules + ["Controllers"]) do |buffer|
|
|
33
|
+
emit_controller(buffer, tag, operations)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
sig { params(buffer: Buffer, tag: String, operations: T::Array[Model::Operation]).void }
|
|
41
|
+
def emit_controller(buffer, tag, operations)
|
|
42
|
+
buffer.nest("class #{Naming.pascal(tag)}Controller < #{controller_base}") do
|
|
43
|
+
buffer.line("extend T::Sig")
|
|
44
|
+
buffer.line("include ::OpenAPIKit::Rendering")
|
|
45
|
+
|
|
46
|
+
operations.each do |operation|
|
|
47
|
+
buffer.blank
|
|
48
|
+
emit_action(buffer, operation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
buffer.blank
|
|
52
|
+
buffer.line("private")
|
|
53
|
+
buffer.blank
|
|
54
|
+
emit_handler(buffer, tag)
|
|
55
|
+
|
|
56
|
+
operations.select { |operation| principal?(operation) }.each do |operation|
|
|
57
|
+
buffer.blank
|
|
58
|
+
emit_authenticator(buffer, operation)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { returns(String) }
|
|
64
|
+
def controller_base
|
|
65
|
+
base = @config.controller_base
|
|
66
|
+
base.start_with?("::") ? base : "::#{base}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { params(buffer: Buffer, tag: String).void }
|
|
70
|
+
def emit_handler(buffer, tag)
|
|
71
|
+
interface = "#{@config.namespace}::Handlers::#{Handlers.module_name(tag)}"
|
|
72
|
+
|
|
73
|
+
buffer.line("sig { returns(#{interface}) }")
|
|
74
|
+
buffer.line("def handler = #{@config.namespace}.registry.#{Naming.snake(tag)}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
78
|
+
def emit_action(buffer, operation)
|
|
79
|
+
scope = "#{@config.namespace}::Operations::#{Operations.module_name(operation)}"
|
|
80
|
+
groups = grouped_parameters(operation)
|
|
81
|
+
|
|
82
|
+
buffer.line("sig { void }")
|
|
83
|
+
buffer.nest("def #{Naming.identifier(operation.id)}") do
|
|
84
|
+
emit_authentication(buffer, operation)
|
|
85
|
+
|
|
86
|
+
groups.each { |name, parameters| buffer.line(source_line(name, parameters)) }
|
|
87
|
+
buffer.blank unless groups.empty?
|
|
88
|
+
|
|
89
|
+
emit_decoded(buffer, operation, scope, groups)
|
|
90
|
+
buffer.blank
|
|
91
|
+
emit_dispatch(buffer, operation)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
96
|
+
def emit_authentication(buffer, operation)
|
|
97
|
+
return unless principal?(operation)
|
|
98
|
+
|
|
99
|
+
buffer.line("principal = #{authenticator_name(operation)}")
|
|
100
|
+
buffer.blank
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { params(operation: Model::Operation).returns(String) }
|
|
104
|
+
def authenticator_name(operation) = "authenticate_#{Naming.identifier(operation.id)}"
|
|
105
|
+
|
|
106
|
+
# Each alternative is one attempt, tried in the order the document lists them.
|
|
107
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
108
|
+
def emit_authenticator(buffer, operation)
|
|
109
|
+
requirements = @document.security_for(operation)
|
|
110
|
+
|
|
111
|
+
buffer.line("sig { returns(#{Security.principal_type(requirements, @config)}) }")
|
|
112
|
+
buffer.nest("def #{authenticator_name(operation)}") do
|
|
113
|
+
buffer.line("::OpenAPIKit::Security.first_of(")
|
|
114
|
+
buffer.indent do
|
|
115
|
+
buffer.line("[")
|
|
116
|
+
buffer.indent do
|
|
117
|
+
requirements.reject(&:anonymous?).each { |requirement| buffer.line("#{attempt(requirement)},") }
|
|
118
|
+
end
|
|
119
|
+
buffer.line("]")
|
|
120
|
+
end
|
|
121
|
+
buffer.line(requirements.any?(&:anonymous?) ? ")" : ") || raise(::OpenAPIKit::SecurityError)")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sig { params(requirement: Model::SecurityRequirement).returns(String) }
|
|
126
|
+
def attempt(requirement)
|
|
127
|
+
name = T.must(requirement.schemes.keys.first)
|
|
128
|
+
scopes = T.must(requirement.schemes[name])
|
|
129
|
+
reader = "#{@config.namespace}.registry.#{Naming.snake(name)}"
|
|
130
|
+
|
|
131
|
+
"-> { #{reader}.authenticate(request: request, scopes: #{scopes.inspect}) }"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
sig do
|
|
135
|
+
params(buffer: Buffer, operation: Model::Operation, scope: String,
|
|
136
|
+
groups: T::Hash[String, T::Array[Model::Parameter]]).void
|
|
137
|
+
end
|
|
138
|
+
def emit_decoded(buffer, operation, scope, groups)
|
|
139
|
+
buffer.line("decoded = #{scope}::Request.new(")
|
|
140
|
+
buffer.indent do
|
|
141
|
+
groups.each_key do |name|
|
|
142
|
+
buffer.line("#{Naming.identifier(name)}: #{scope}::#{name}.new(")
|
|
143
|
+
buffer.indent { emit_group_arguments(buffer, name, T.must(groups[name])) }
|
|
144
|
+
buffer.line("),")
|
|
145
|
+
end
|
|
146
|
+
buffer.line("body: #{body_expression(operation)},") if body?(operation)
|
|
147
|
+
buffer.line("principal: principal,") if principal?(operation)
|
|
148
|
+
buffer.line("http_request: request")
|
|
149
|
+
end
|
|
150
|
+
buffer.line(")")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { params(buffer: Buffer, operation: Model::Operation).void }
|
|
154
|
+
def emit_dispatch(buffer, operation)
|
|
155
|
+
buffer.line("render_response(handler.#{Naming.identifier(operation.id)}(request: decoded))")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
sig { params(buffer: Buffer, group: String, parameters: T::Array[Model::Parameter]).void }
|
|
159
|
+
def emit_group_arguments(buffer, group, parameters)
|
|
160
|
+
source = T.must(SOURCES[group])
|
|
161
|
+
parameters.each do |parameter|
|
|
162
|
+
info = Model::Parameter.info(parameter)
|
|
163
|
+
expression = Decode.expression(source: source, key: info.name, schema: info.schema,
|
|
164
|
+
required: Model::Parameter.required?(parameter), registry: @registry)
|
|
165
|
+
buffer.line("#{info.identifier}: #{expression},")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { params(group: String, parameters: T::Array[Model::Parameter]).returns(String) }
|
|
170
|
+
def source_line(group, parameters)
|
|
171
|
+
names = parameters.map { |parameter| Model::Parameter.info(parameter).name }
|
|
172
|
+
|
|
173
|
+
"#{T.must(SOURCES[group])} = ::OpenAPIKit::Decode.gather(#{names.inspect}) " \
|
|
174
|
+
"{ |name| #{lookup(group)} }"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sig { params(group: String).returns(String) }
|
|
178
|
+
def lookup(group)
|
|
179
|
+
case group
|
|
180
|
+
when "Path" then "request.path_parameters[name.to_sym]"
|
|
181
|
+
when "Query" then "request.query_parameters[name]"
|
|
182
|
+
when "Headers" then "request.headers[name]"
|
|
183
|
+
when "Cookies" then "request.cookies[name]"
|
|
184
|
+
else raise SchemaError, "unknown parameter group #{group}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
sig { params(operation: Model::Operation).returns(T::Hash[String, T::Array[Model::Parameter]]) }
|
|
189
|
+
def grouped_parameters(operation)
|
|
190
|
+
reject_unsupported_styles(operation)
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
"Path" => Model::PathParameter, "Query" => Model::QueryParameter,
|
|
194
|
+
"Headers" => Model::HeaderParameter, "Cookies" => Model::CookieParameter
|
|
195
|
+
}.filter_map do |name, kind|
|
|
196
|
+
found = operation.parameters.grep(kind)
|
|
197
|
+
found.empty? ? nil : [name, found]
|
|
198
|
+
end.to_h
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
sig { params(operation: Model::Operation).void }
|
|
202
|
+
def reject_unsupported_styles(operation)
|
|
203
|
+
operation.parameters.each do |parameter|
|
|
204
|
+
style = unsupported_style(parameter)
|
|
205
|
+
next if style.nil?
|
|
206
|
+
|
|
207
|
+
raise SchemaError,
|
|
208
|
+
"#{operation.id} declares parameter " \
|
|
209
|
+
"#{Model::Parameter.info(parameter).name.inspect} with style #{style.inspect}, which " \
|
|
210
|
+
"openapi_kit does not decode yet. Supported styles are simple for path and form for query."
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
sig { params(parameter: Model::Parameter).returns(T.nilable(String)) }
|
|
215
|
+
def unsupported_style(parameter)
|
|
216
|
+
case parameter
|
|
217
|
+
when Model::PathParameter
|
|
218
|
+
parameter.style == Model::PathStyle::Simple ? nil : parameter.style.serialize
|
|
219
|
+
when Model::QueryParameter
|
|
220
|
+
parameter.style == Model::QueryStyle::Form ? nil : parameter.style.serialize
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
sig { params(operation: Model::Operation).returns(T::Boolean) }
|
|
225
|
+
def principal?(operation)
|
|
226
|
+
!@config.principal.nil? && !@document.security_for(operation).empty?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
sig { params(operation: Model::Operation).returns(T::Boolean) }
|
|
230
|
+
def body?(operation)
|
|
231
|
+
body = operation.request_body
|
|
232
|
+
!body.nil? && !body.contents.empty?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
sig { params(operation: Model::Operation).returns(String) }
|
|
236
|
+
def body_expression(operation)
|
|
237
|
+
body = T.must(operation.request_body)
|
|
238
|
+
schema = T.must(body.contents.first).schema
|
|
239
|
+
return "nil" if schema.nil?
|
|
240
|
+
|
|
241
|
+
source = body_source(schema)
|
|
242
|
+
inner = @registry.from_form_expr(schema, value: source) ||
|
|
243
|
+
@registry.from_wire_expr(schema, value: source)
|
|
244
|
+
body.required ? inner : "(#{inner} if #{body_present(schema)})"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Rails' JSON parameter parser wraps a body that is not a JSON object as
|
|
248
|
+
# { _json: parsed }, so a top-level array or scalar arrives under that key.
|
|
249
|
+
sig { params(schema: Model::Schema).returns(String) }
|
|
250
|
+
def body_source(schema)
|
|
251
|
+
@registry.object?(schema) ? "request.request_parameters" : %(request.request_parameters["_json"])
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
sig { params(schema: Model::Schema).returns(String) }
|
|
255
|
+
def body_present(schema)
|
|
256
|
+
return "request.request_parameters.any?" if @registry.object?(schema)
|
|
257
|
+
|
|
258
|
+
%(request.request_parameters.key?("_json"))
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
module Decode
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { params(required: T::Boolean, meta: Model::Meta).returns(T::Boolean) }
|
|
11
|
+
def self.optional_nullable?(required:, meta:) = !required && meta.nullable && meta.default.nil?
|
|
12
|
+
|
|
13
|
+
sig do
|
|
14
|
+
params(source: String, key: String, schema: Model::Schema, required: T::Boolean,
|
|
15
|
+
registry: TypeRegistry).returns(String)
|
|
16
|
+
end
|
|
17
|
+
def self.expression(source:, key:, schema:, required:, registry:)
|
|
18
|
+
meta = Model::Schema.meta(schema)
|
|
19
|
+
inner = if Model::Schema.file?(schema)
|
|
20
|
+
"::OpenAPIKit::Decode.file(v)"
|
|
21
|
+
else
|
|
22
|
+
registry.from_wire_expr(schema, value: "v")
|
|
23
|
+
end
|
|
24
|
+
quoted = key.inspect
|
|
25
|
+
default = meta.default
|
|
26
|
+
|
|
27
|
+
if default && !required
|
|
28
|
+
fallback = Defaults.expression(schema: schema, default: default, registry: registry)
|
|
29
|
+
return "::OpenAPIKit::Decode.defaulted(#{source}, #{quoted}, #{fallback}) { |v| #{inner} }"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return "::OpenAPIKit::Decode.optional_nullable(#{source}, #{quoted}) { |v| #{inner} }" if
|
|
33
|
+
optional_nullable?(required: required, meta: meta)
|
|
34
|
+
return "::OpenAPIKit::Decode.required_nullable(#{source}, #{quoted}) { |v| #{inner} }" if
|
|
35
|
+
required && meta.nullable
|
|
36
|
+
return "::OpenAPIKit::Decode.required(#{source}, #{quoted}) { |v| #{inner} }" if required
|
|
37
|
+
|
|
38
|
+
"::OpenAPIKit::Decode.optional(#{source}, #{quoted}) { |v| #{inner} }"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
module Defaults
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig do
|
|
11
|
+
params(schema: Model::Schema, default: Model::Default, registry: TypeRegistry).returns(String)
|
|
12
|
+
end
|
|
13
|
+
def self.expression(schema:, default:, registry:)
|
|
14
|
+
literal = default.value
|
|
15
|
+
return "nil" if literal.nil?
|
|
16
|
+
return literal.inspect if registry.literal_matches_type?(schema, literal)
|
|
17
|
+
|
|
18
|
+
registry.from_wire_expr(schema, value: literal.inspect)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig do
|
|
22
|
+
params(schema: Model::Schema, default: Model::Default, registry: TypeRegistry).returns(String)
|
|
23
|
+
end
|
|
24
|
+
def self.clause(schema:, default:, registry:)
|
|
25
|
+
literal = default.value
|
|
26
|
+
return "default: nil" if literal.nil?
|
|
27
|
+
return "default: #{literal.inspect}" if registry.literal_matches_type?(schema, literal)
|
|
28
|
+
|
|
29
|
+
"factory: -> { #{expression(schema: schema, default: default, registry: registry)} }"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(required: T::Boolean, meta: Model::Meta).returns(T::Boolean) }
|
|
33
|
+
def self.nilable?(required:, meta:)
|
|
34
|
+
default = meta.default
|
|
35
|
+
return true if default && default.value.nil?
|
|
36
|
+
|
|
37
|
+
meta.nullable || (!required && default.nil?)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
module Emitter
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
interface!
|
|
11
|
+
|
|
12
|
+
sig { abstract.returns(T::Array[SourceFile]) }
|
|
13
|
+
def render; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module OpenAPIKit
|
|
5
|
+
module Codegen
|
|
6
|
+
module Emit
|
|
7
|
+
class Forms
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { params(registry: TypeRegistry).void }
|
|
11
|
+
def initialize(registry:)
|
|
12
|
+
@registry = registry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { params(buffer: Buffer, type: Model::FormDef).void }
|
|
16
|
+
def emit(buffer, type)
|
|
17
|
+
qualified = @registry.sorbet_type(Model::Ref.new(name: type.name))
|
|
18
|
+
|
|
19
|
+
buffer.nest("module Form") do
|
|
20
|
+
buffer.line("extend T::Sig")
|
|
21
|
+
buffer.line("extend T::Generic")
|
|
22
|
+
buffer.line("extend ::OpenAPIKit::Form::Contract")
|
|
23
|
+
buffer.blank
|
|
24
|
+
buffer.line("Value = type_template { { fixed: #{qualified} } }")
|
|
25
|
+
buffer.blank
|
|
26
|
+
buffer.line("sig { override.params(parts: ::OpenAPIKit::Form::Parts).returns(#{qualified}) }")
|
|
27
|
+
buffer.nest("def self.from_parts(parts)") do
|
|
28
|
+
buffer.line("#{qualified}.new(")
|
|
29
|
+
buffer.indent do
|
|
30
|
+
type.properties.each { |property| buffer.line("#{property.identifier}: #{decode(property)},") }
|
|
31
|
+
extra = type.additional_properties
|
|
32
|
+
buffer.line("additional_properties: #{decode_extra(type, extra)},") if extra
|
|
33
|
+
end
|
|
34
|
+
buffer.line(")")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
sig { params(property: Model::Property).returns(String) }
|
|
42
|
+
def decode(property)
|
|
43
|
+
Decode.expression(source: "parts", key: property.name, schema: property.schema,
|
|
44
|
+
required: property.required, registry: @registry)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(type: Model::FormDef, schema: Model::Schema).returns(String) }
|
|
48
|
+
def decode_extra(type, schema)
|
|
49
|
+
known = type.properties.map { |property| property.name.inspect }.join(", ")
|
|
50
|
+
inner = @registry.from_wire_expr(schema, value: "item")
|
|
51
|
+
"::OpenAPIKit::Decode.values(parts.except(#{known})) { |item| #{inner} }"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|