openapi_kit 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6065eebfc91e1494e62d6ee4bd5986e4cd65793dff51552bf237e6834233e663
4
+ data.tar.gz: 9f08ae0a11555959f1a7bb35641c6b9a6e97992bc04d8cd38744699cf161bf0b
5
+ SHA512:
6
+ metadata.gz: a165502bb0a88de4442110f3813026f8f4af796213830f2b9ee77ec83fa7294e0ce299139651ab41ec2fa8a81fb039bc69b3acb88723638bda3ce2bdb4f3be0f
7
+ data.tar.gz: a31c6a518a580629fc9a772c786bc22aaf6738c05ef12c34156368decf0d4ef23202dea77f4b2dfb2fd96677c56bedbabf52fe85c2e76c992a996c10fd07a7c3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Nexus Mods
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,385 @@
1
+ # openapi_kit
2
+
3
+ Generates Sorbet-typed Rails server stubs from an OpenAPI 3 document. Handlers are strict
4
+ interfaces bound through a generated registry, responses are sealed, and authentication is
5
+ enforced where the document says it should be. Change the document and the build tells you
6
+ what no longer compiles.
7
+
8
+ ```ruby
9
+ gem "openapi_kit" # what generated code calls
10
+
11
+ group :development do
12
+ gem "openapi_kit-codegen" # the generator
13
+ end
14
+ ```
15
+
16
+ ## Contents
17
+
18
+ - [Generating](#generating)
19
+ - [Integrating with Rails](#integrating-with-rails)
20
+ - [Security](#security)
21
+ - [Custom types](#custom-types)
22
+ - [Files and binary responses](#files-and-binary-responses)
23
+ - [Not supported yet](#not-supported-yet)
24
+ - [Development](#development)
25
+
26
+ ## Generating
27
+
28
+ ```yaml
29
+ # openapi_kit.yml
30
+ spec: openapi/petstore.yaml
31
+ output: app/api
32
+ modules: [Petstore, V1]
33
+ controller_base: Api::BaseController
34
+ principal: "::Petstore::Principal"
35
+ ```
36
+
37
+ | Option | Meaning |
38
+ | --- | --- |
39
+ | `spec` | root OpenAPI document, resolved relative to this file |
40
+ | `output` | directory the tree is written to, which openapi_kit owns |
41
+ | `modules` | namespace for every generated constant, so `Petstore::V1::Types::Pet` |
42
+ | `controller_base` | class the generated controllers inherit from |
43
+ | `principal` | the class a successful authentication produces |
44
+ | `type_mappings` | your Ruby type for a `type:format` pair |
45
+ | `name_overrides` | a different Ruby name for a schema |
46
+
47
+ The first four are required.
48
+
49
+ ```console
50
+ $ bundle exec openapi_kit generate -c openapi_kit.yml
51
+ app/api/petstore/v1/types/pet.rb
52
+ app/api/petstore/v1/operations/list_pets.rb
53
+ app/api/petstore/v1/handlers/pets.rb
54
+ app/api/petstore/v1/controllers/pets_controller.rb
55
+ app/api/petstore/v1/security.rb
56
+ app/api/petstore/v1/registry.rb
57
+ app/api/petstore/v1/routes.rb
58
+ ```
59
+
60
+ One constant per file at the path that constant implies, so Rails autoloads it. Each run
61
+ wipes `output`, but only after checking openapi_kit generated every `.rb` in it.
62
+
63
+ ## Integrating with Rails
64
+
65
+ **Autoload the output**, if it is not already under `app/`. An acronym in `modules`
66
+ becomes a directory, so it needs an inflection like any other acronym constant.
67
+
68
+ ```ruby
69
+ # config/application.rb
70
+ config.autoload_paths << Rails.root.join("app/api").to_s
71
+ ```
72
+
73
+ **Draw the routes.** They carry no prefix of their own, so mount them where you like.
74
+
75
+ ```ruby
76
+ # config/routes.rb
77
+ Rails.application.routes.draw do
78
+ scope "/v1" do
79
+ Petstore::V1::Routes.draw(self)
80
+ end
81
+ end
82
+ ```
83
+
84
+ **Implement one handler per tag.** Miss an operation and Sorbet names the abstract
85
+ method. Return an undeclared response variant and it will not compile.
86
+
87
+ ```ruby
88
+ class PetsHandler
89
+ extend T::Sig
90
+ include Petstore::V1::Handlers::Pets
91
+
92
+ sig do
93
+ override.params(request: Petstore::V1::Operations::ListPets::Request)
94
+ .returns(Petstore::V1::Operations::ListPets::Response)
95
+ end
96
+ def list_pets(request:)
97
+ pets = Pet.where(store: request.path.store_id).page(request.query.page)
98
+
99
+ Petstore::V1::Operations::ListPets::Ok.new(body: pets.map { |pet| present(pet) })
100
+ end
101
+ end
102
+ ```
103
+
104
+ **Give the controllers a base class.** They inherit whatever you put there and need
105
+ nothing from it, so this is where your own concerns and error mapping live. A request
106
+ openapi_kit cannot decode raises `OpenAPIKit::DecodeError`, carrying `detail` and a `json_pointer`
107
+ naming the field, and openapi_kit takes no view on the wire format.
108
+
109
+ ```ruby
110
+ # app/controllers/api/base_controller.rb
111
+ module Api
112
+ class BaseController < ApplicationController
113
+ rescue_from OpenAPIKit::DecodeError, with: :unprocessable
114
+ rescue_from OpenAPIKit::SecurityError, with: :unauthorized
115
+
116
+ private
117
+
118
+ def unauthorized = head(:unauthorized)
119
+
120
+ def unprocessable(error)
121
+ render json: { detail: error.detail, pointer: error.json_pointer },
122
+ status: 422
123
+ end
124
+ end
125
+ end
126
+ ```
127
+
128
+ **Assign the registry.** openapi_kit generates a `Registry` struct with one slot per handler and
129
+ authenticator, and controllers read it from `Petstore::V1.registry`. Rails instantiates
130
+ controllers itself, so they cannot be handed one. Omit a slot, or pass something that does
131
+ not implement its interface, and it does not compile.
132
+
133
+ ```ruby
134
+ # config/initializers/openapi_kit.rb
135
+ Rails.application.config.to_prepare do
136
+ Petstore::V1.registry = Petstore::V1::Registry.new(
137
+ pets: PetsHandler.new(repo: PetRepo.new),
138
+ system: SystemHandler.new,
139
+ bearer_auth: BearerAuthenticator.new(decoder: TokenDecoder.new)
140
+ )
141
+ end
142
+ ```
143
+
144
+ The registry is openapi_kit's boundary and nothing more. What a handler needs behind it is
145
+ yours, and unlike controllers *you* construct handlers, so they take whatever they need.
146
+
147
+ Building the registry in `to_prepare` constructs every handler at boot, along with
148
+ whatever their constructors resolve, and reassigns them on a code reload.
149
+
150
+ ## Security
151
+
152
+ Where the document declares `security`, the generated action authenticates before decoding
153
+ anything, and your handler receives the principal. Nothing else can reach the handler.
154
+
155
+ ```yaml
156
+ security: [{ bearerAuth: [] }] # the document's default
157
+ paths:
158
+ /stores/{storeId}/pets:
159
+ post:
160
+ security: # this operation overrides it
161
+ - bearerAuth: [pets:write]
162
+ - apiKeyAuth: []
163
+ ```
164
+
165
+ Name what authentication produces, and seal it if your schemes produce different shapes:
166
+ that is what makes a handler's `case` exhaustive.
167
+
168
+ ```ruby
169
+ module Petstore::Principal
170
+ extend T::Helpers
171
+ sealed!
172
+
173
+ class Token < T::Struct # a JWT carries its permissions
174
+ include Petstore::Principal
175
+ const :user_id, Integer
176
+ const :permissions, T::Array[String]
177
+ end
178
+
179
+ class Key < T::Struct # an API key does not
180
+ include Petstore::Principal
181
+ const :client, String
182
+ end
183
+ end
184
+ ```
185
+
186
+ Then write one authenticator per scheme. Return `nil` to say this alternative was not
187
+ satisfied, so openapi_kit tries the next one. Where the credential lives is what the document
188
+ declares, so `credential` is implemented for you.
189
+
190
+ ```ruby
191
+ class BearerAuthenticator
192
+ extend T::Sig
193
+ include Petstore::V1::Security::BearerAuth
194
+
195
+ sig { params(decoder: TokenDecoder).void }
196
+ def initialize(decoder:)
197
+ @decoder = decoder
198
+ end
199
+
200
+ sig do
201
+ override.params(request: ActionDispatch::Request, scopes: T::Array[String])
202
+ .returns(T.nilable(Petstore::Principal::Token))
203
+ end
204
+ def authenticate(request:, scopes:)
205
+ token = credential(request) or return nil
206
+ claims = @decoder.decode(token) or return nil
207
+ return nil unless scopes.all? { |scope| claims.scopes.include?(scope) }
208
+
209
+ Petstore::Principal::Token.new(user_id: claims.sub, permissions: claims.scopes)
210
+ end
211
+ end
212
+ ```
213
+
214
+ `credential` reads the `Authorization` header and strips the declared scheme for `http`,
215
+ `oauth2` and `openIdConnect`, and reads the named header, query parameter or cookie for
216
+ `apiKey`. A `basic` scheme also gets `basic_credential`, returning the decoded
217
+ `[user, password]`.
218
+
219
+ Your handler then reads `request.principal`:
220
+
221
+ ```ruby
222
+ def create_pet(request:)
223
+ owner = case request.principal
224
+ when Petstore::Principal::Token then "user-#{request.principal.user_id}"
225
+ when Petstore::Principal::Key then request.principal.client
226
+ else T.absurd(request.principal)
227
+ end
228
+ end
229
+ ```
230
+
231
+ Alternatives are tried in document order and the first to produce a principal wins. If
232
+ none do, openapi_kit raises `OpenAPIKit::SecurityError`. An operation offering anonymous
233
+ access (`security: [..., {}]`) makes the context `T.nilable` and raises nothing.
234
+
235
+ ### What each scheme type gives you
236
+
237
+ `SCHEMES` is the document's `securitySchemes` as typed values, so an authenticator reads
238
+ its own configuration rather than restating the document.
239
+
240
+ | `type` | The scheme carries |
241
+ | --- | --- |
242
+ | `http` | `scheme`, `bearer_format` |
243
+ | `apiKey` | `location`, `parameter_name` |
244
+ | `oauth2` | the declared `scopes` catalogue |
245
+ | `openIdConnect` | `url`, the discovery document |
246
+
247
+ Plus any `x-` keys, on all four. OpenAPI 3.0 fixes these four types, so a bespoke scheme
248
+ is an `http` one with your own name, and anything the type cannot express goes in `x-`:
249
+
250
+ ```yaml
251
+ securitySchemes:
252
+ hmacAuth:
253
+ type: http
254
+ scheme: HMAC-SHA256
255
+ x-signing-key: SIGNING_KEY
256
+ ```
257
+
258
+ That reaches the scheme as `extensions`, which is also where an `oauth2` scheme's issuer
259
+ and JWKS URI belong, since OpenAPI has no field for them. An `openIdConnect` scheme needs
260
+ neither, because `url` discovers both.
261
+
262
+ ## Custom types
263
+
264
+ A `type:format` pair with no built-in mapping is an error naming the config to add:
265
+
266
+ ```yaml
267
+ type_mappings:
268
+ "string:money":
269
+ type: "::Money"
270
+ codec: "MyApp::MoneyCodec"
271
+ ```
272
+
273
+ `type` appears in signatures. `codec` converts it, and `Value` ties the halves together so
274
+ a codec cannot decode one type and encode another:
275
+
276
+ ```ruby
277
+ module MyApp::MoneyCodec
278
+ extend T::Sig
279
+ extend T::Generic
280
+ extend OpenAPIKit::Codec::Contract
281
+
282
+ Value = type_template { { fixed: ::Money } }
283
+
284
+ sig { override.params(value: OpenAPIKit::Wire).returns(::Money) }
285
+ def self.from_wire(value) = ::Money.parse(OpenAPIKit::Codec::String.from_wire(value))
286
+
287
+ sig { override.params(value: ::Money).returns(OpenAPIKit::Wire) }
288
+ def self.to_wire(value) = value.format
289
+ end
290
+ ```
291
+
292
+ `codec` need only name a constant answering `from_wire` and `to_wire`, so a codec that
293
+ needs configuration can `include` the contract instead and you name the instance you
294
+ built. The same override works inline, for one property rather than every occurrence of a
295
+ format:
296
+
297
+ ```yaml
298
+ price:
299
+ type: string
300
+ x-ruby-type: "::Money"
301
+ x-ruby-codec: "MyApp::MoneyCodec"
302
+ ```
303
+
304
+ ## Files and binary responses
305
+
306
+ A file is the one thing outside `OpenAPIKit::Wire`, the value model every media type shares,
307
+ so it never goes through a codec. `format: binary` is handled in two places instead, and
308
+ refused everywhere else.
309
+
310
+ **An upload is a property of a `multipart/form-data` request body**, decoded through a
311
+ `Form` where other types carry a `Codec`:
312
+
313
+ ```ruby
314
+ class UploadPetPhotoBody < T::Struct
315
+ const :photo, ::ActionDispatch::Http::UploadedFile
316
+ const :description, T.nilable(::String)
317
+ end
318
+
319
+ def upload_pet_photo(request:)
320
+ Blob.store!(io: request.body.photo.tempfile)
321
+
322
+ Petstore::V1::Operations::UploadPetPhoto::NoContent.new
323
+ end
324
+ ```
325
+
326
+ **A binary response is the whole body**, taking a file on disk or a block that writes bytes:
327
+
328
+ ```ruby
329
+ body: OpenAPIKit::Body::File.new(path: Rails.root.join("photos", name))
330
+
331
+ body: OpenAPIKit::Body::Stream.new(
332
+ body: ->(sink) { Archive.open(pet) { |zip| IO.copy_stream(zip, sink) } }
333
+ )
334
+
335
+ body: OpenAPIKit::Body::Stream.new(body: ->(sink) { sink << header << row })
336
+ ```
337
+
338
+ A path names the file and nothing more, so the server sends it however it likes: `sendfile`
339
+ under Puma, an `X-Accel-Redirect` or `X-Sendfile` under nginx and Apache through
340
+ `Rack::Sendfile`, and `Content-Length` comes off the file. A stream sends no
341
+ `Content-Length`, and whatever its block opens it also closes. Neither supports `Range`.
342
+
343
+ Every response variant answers `to_body`, a sealed `OpenAPIKit::Body` of `Empty`, `Json`,
344
+ `Stream` or `File`. `string:binary` takes no [`type_mappings`](#custom-types) entry, since a
345
+ file has nothing for a codec to convert.
346
+
347
+ ## Not supported yet
348
+
349
+ Refused at generation time, rather than mis-generated:
350
+
351
+ - Parameter styles other than `simple` for path and `form` for query.
352
+ - One content type per request body, and it must be `application/json`, a `+json` type,
353
+ `application/x-www-form-urlencoded` or `multipart/form-data`. A response body whose
354
+ schema is `format: binary` may declare any content type at all.
355
+ - Two security schemes required together in one alternative (`{a: [], b: []}`). One scheme
356
+ per alternative.
357
+ - A path template Rails cannot route, such as `{pet-id}`.
358
+
359
+ Documented behaviour to know about:
360
+
361
+ - Array and object query parameters follow Rails' conventions, not OpenAPI's: send
362
+ `?tags[]=a&tags[]=b` and `?filter[lat]=1`, not `?tags=a&tags=b` or an exploded `?lat=1`.
363
+ - Schema keyword validation (`minLength`, `pattern`, `minimum`) is not enforced. Types and
364
+ formats only.
365
+ - Codecs coerce strings, since path, query and header values arrive as strings. That
366
+ leniency also applies to bodies, so `{"count": "42"}` satisfies `type: integer`.
367
+ - `format: binary` is only valid as a top-level property of a `multipart/form-data`
368
+ request body, or as the whole schema of a response body. Anywhere else is refused: a
369
+ file is bytes rather than a parsed value, so no codec can convert it. Use `format: byte`
370
+ to carry bytes inside a value.
371
+ - An OAuth2 flow's `authorizationUrl`, `tokenUrl` and `refreshUrl` are not carried, since
372
+ they tell a client where to obtain a token and a resource server never calls them.
373
+
374
+ ## Development
375
+
376
+ ```console
377
+ $ bundle exec rake golden # regenerate the output the specs compare against
378
+ $ bundle exec rake # rspec, srb tc, rubocop
379
+ ```
380
+
381
+ `srb tc` covers `spec/golden` as well as the generator, so output that does not typecheck
382
+ fails the build. `spec/dummy` is a Rails application whose `app/api` is generated the same
383
+ way, and `spec/generated/rails_request_spec.rb` issues real requests against it.
384
+
385
+ [ARCHITECTURE.md](ARCHITECTURE.md) covers the pipeline and where to change what.
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+
6
+ require "openapi_kit/wire"
7
+
8
+ module OpenAPIKit
9
+ class Sink
10
+ extend T::Sig
11
+
12
+ sig { params(block: T.proc.params(bytes: ::String).void).void }
13
+ def initialize(block)
14
+ @block = block
15
+ end
16
+
17
+ sig { params(bytes: ::String).returns(::Integer) }
18
+ def write(bytes)
19
+ @block.call(bytes.b)
20
+ bytes.bytesize
21
+ end
22
+
23
+ sig { params(bytes: ::String).returns(T.self_type) }
24
+ def <<(bytes)
25
+ write(bytes)
26
+ self
27
+ end
28
+
29
+ sig { returns(T.self_type) }
30
+ def flush = self
31
+ end
32
+
33
+ module Body
34
+ extend T::Sig
35
+ extend T::Helpers
36
+ abstract!
37
+ sealed!
38
+
39
+ class Empty < T::Struct
40
+ include Body
41
+ end
42
+
43
+ class Json < T::Struct
44
+ include Body
45
+
46
+ const :wire, OpenAPIKit::Wire
47
+ end
48
+
49
+ class Stream < T::Struct
50
+ extend T::Sig
51
+ include Body
52
+
53
+ const :body, T.proc.params(sink: OpenAPIKit::Sink).void
54
+
55
+ sig { params(block: T.proc.params(bytes: ::String).void).void }
56
+ def each(&block) = body.call(Sink.new(block))
57
+ end
58
+
59
+ class File < T::Struct
60
+ extend T::Sig
61
+ include Body
62
+
63
+ const :path, ::Pathname
64
+
65
+ sig { returns(::Integer) }
66
+ def size = path.size
67
+ end
68
+
69
+ Binary = T.type_alias { T.any(OpenAPIKit::Body::Stream, OpenAPIKit::Body::File) }
70
+ end
71
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Boolean
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: T::Boolean } }
14
+
15
+ TRUTHY = T.let(%w[true 1].freeze, T::Array[::String])
16
+ FALSEY = T.let(%w[false 0].freeze, T::Array[::String])
17
+
18
+ sig { override.params(value: OpenAPIKit::Wire).returns(T::Boolean) }
19
+ def self.from_wire(value)
20
+ return value if value.is_a?(::TrueClass) || value.is_a?(::FalseClass)
21
+
22
+ if value.is_a?(::String)
23
+ return true if TRUTHY.include?(value.downcase)
24
+ return false if FALSEY.include?(value.downcase)
25
+ end
26
+
27
+ raise DecodeError.new("expected a boolean, got #{value.inspect}")
28
+ end
29
+
30
+ sig { override.params(value: T::Boolean).returns(OpenAPIKit::Wire) }
31
+ def self.to_wire(value) = value
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/codec/contract"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Byte
9
+ extend T::Sig
10
+ extend T::Generic
11
+ extend Contract
12
+
13
+ Value = type_template { { fixed: ::String } }
14
+
15
+ sig { override.params(value: OpenAPIKit::Wire).returns(::String) }
16
+ def self.from_wire(value)
17
+ raise DecodeError.new("expected base64, got #{value.inspect}") unless value.is_a?(::String)
18
+
19
+ decoded = value.unpack1("m0")
20
+ raise DecodeError.new("expected base64, got #{value.inspect}") if decoded.nil?
21
+
22
+ decoded
23
+ rescue ArgumentError
24
+ raise DecodeError.new("expected base64, got #{value.inspect}")
25
+ end
26
+
27
+ sig { override.params(value: ::String).returns(OpenAPIKit::Wire) }
28
+ def self.to_wire(value) = [value].pack("m0")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_kit/wire"
5
+
6
+ module OpenAPIKit
7
+ module Codec
8
+ module Contract
9
+ extend T::Sig
10
+ extend T::Generic
11
+ interface!
12
+
13
+ Value = type_member
14
+
15
+ sig { abstract.params(value: OpenAPIKit::Wire).returns(Value) }
16
+ def from_wire(value); end
17
+
18
+ sig { abstract.params(value: Value).returns(OpenAPIKit::Wire) }
19
+ def to_wire(value); end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "date"
5
+
6
+ require "openapi_kit/codec/contract"
7
+
8
+ module OpenAPIKit
9
+ module Codec
10
+ module Date
11
+ extend T::Sig
12
+ extend T::Generic
13
+ extend Contract
14
+
15
+ Value = type_template { { fixed: ::Date } }
16
+
17
+ sig { override.params(value: OpenAPIKit::Wire).returns(::Date) }
18
+ def self.from_wire(value)
19
+ raise DecodeError.new("expected an ISO 8601 date, got #{value.inspect}") unless value.is_a?(::String)
20
+
21
+ begin
22
+ ::Date.iso8601(value)
23
+ rescue ::Date::Error
24
+ raise DecodeError.new("expected an ISO 8601 date, got #{value.inspect}")
25
+ end
26
+ end
27
+
28
+ sig { override.params(value: ::Date).returns(OpenAPIKit::Wire) }
29
+ def self.to_wire(value) = value.iso8601
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+
6
+ require "openapi_kit/codec/contract"
7
+
8
+ module OpenAPIKit
9
+ module Codec
10
+ module DateTime
11
+ extend T::Sig
12
+ extend T::Generic
13
+ extend Contract
14
+
15
+ Value = type_template { { fixed: ::Time } }
16
+
17
+ sig { override.params(value: OpenAPIKit::Wire).returns(::Time) }
18
+ def self.from_wire(value)
19
+ raise DecodeError.new("expected an RFC 3339 date-time, got #{value.inspect}") unless
20
+ value.is_a?(::String)
21
+
22
+ begin
23
+ ::Time.iso8601(value)
24
+ rescue ArgumentError
25
+ raise DecodeError.new("expected an RFC 3339 date-time, got #{value.inspect}")
26
+ end
27
+ end
28
+
29
+ sig { override.params(value: ::Time).returns(OpenAPIKit::Wire) }
30
+ def self.to_wire(value) = value.utc.iso8601
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bigdecimal"
5
+
6
+ require "openapi_kit/codec/contract"
7
+
8
+ module OpenAPIKit
9
+ module Codec
10
+ module Decimal
11
+ extend T::Sig
12
+ extend T::Generic
13
+ extend Contract
14
+
15
+ Value = type_template { { fixed: ::BigDecimal } }
16
+
17
+ sig { override.params(value: OpenAPIKit::Wire).returns(::BigDecimal) }
18
+ def self.from_wire(value)
19
+ case value
20
+ when ::Integer then Kernel.BigDecimal(value)
21
+ when ::Float then Kernel.BigDecimal(value, ::Float::DIG)
22
+ when ::String
23
+ begin
24
+ Kernel.BigDecimal(value)
25
+ rescue ArgumentError, TypeError
26
+ raise DecodeError.new("expected a decimal, got #{value.inspect}")
27
+ end
28
+ else raise DecodeError.new("expected a decimal, got #{value.inspect}")
29
+ end
30
+ end
31
+
32
+ sig { override.params(value: ::BigDecimal).returns(OpenAPIKit::Wire) }
33
+ def self.to_wire(value) = value.to_s("F")
34
+ end
35
+ end
36
+ end