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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +385 -0
  4. data/exe/openapi_kit +6 -0
  5. data/lib/openapi_kit/codegen/cli.rb +72 -0
  6. data/lib/openapi_kit/codegen/config.rb +114 -0
  7. data/lib/openapi_kit/codegen/emit/buffer.rb +92 -0
  8. data/lib/openapi_kit/codegen/emit/codecs.rb +221 -0
  9. data/lib/openapi_kit/codegen/emit/controllers.rb +263 -0
  10. data/lib/openapi_kit/codegen/emit/decode.rb +43 -0
  11. data/lib/openapi_kit/codegen/emit/defaults.rb +42 -0
  12. data/lib/openapi_kit/codegen/emit/emitter.rb +17 -0
  13. data/lib/openapi_kit/codegen/emit/forms.rb +56 -0
  14. data/lib/openapi_kit/codegen/emit/handlers.rb +65 -0
  15. data/lib/openapi_kit/codegen/emit/literal.rb +28 -0
  16. data/lib/openapi_kit/codegen/emit/operations.rb +277 -0
  17. data/lib/openapi_kit/codegen/emit/registry.rb +101 -0
  18. data/lib/openapi_kit/codegen/emit/routes.rb +77 -0
  19. data/lib/openapi_kit/codegen/emit/security.rb +183 -0
  20. data/lib/openapi_kit/codegen/emit/source_file.rb +30 -0
  21. data/lib/openapi_kit/codegen/emit/types.rb +153 -0
  22. data/lib/openapi_kit/codegen/generator.rb +48 -0
  23. data/lib/openapi_kit/codegen/loader.rb +730 -0
  24. data/lib/openapi_kit/codegen/model/document.rb +302 -0
  25. data/lib/openapi_kit/codegen/model/schema.rb +110 -0
  26. data/lib/openapi_kit/codegen/model/type_def.rb +89 -0
  27. data/lib/openapi_kit/codegen/naming.rb +78 -0
  28. data/lib/openapi_kit/codegen/ruby_type.rb +48 -0
  29. data/lib/openapi_kit/codegen/type_registry.rb +325 -0
  30. data/lib/openapi_kit/codegen/writer.rb +92 -0
  31. data/lib/openapi_kit/codegen.rb +37 -0
  32. data/lib/openapi_kit-codegen.rb +4 -0
  33. 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