taro 1.4.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +138 -60
- data/lib/taro/cache.rb +14 -0
- data/lib/taro/common_returns.rb +31 -0
- data/lib/taro/declaration.rb +82 -0
- data/lib/taro/declarations.rb +34 -0
- data/lib/taro/errors.rb +15 -2
- data/lib/taro/export/base.rb +1 -1
- data/lib/taro/export/open_api_v3.rb +20 -23
- data/lib/taro/export.rb +1 -1
- data/lib/taro/none.rb +2 -0
- data/lib/taro/rails/active_declarations.rb +2 -10
- data/lib/taro/rails/declaration.rb +9 -114
- data/lib/taro/rails/declaration_buffer.rb +2 -1
- data/lib/taro/rails/dsl.rb +13 -6
- data/lib/taro/rails/generators/install_generator.rb +1 -1
- data/lib/taro/rails/generators/templates/errors_type.erb +1 -1
- data/lib/taro/rails/generators/templates/response_type.erb +4 -0
- data/lib/taro/rails/generators.rb +1 -1
- data/lib/taro/rails/normalized_route.rb +20 -38
- data/lib/taro/rails/param_parsing.rb +5 -3
- data/lib/taro/rails/railtie.rb +4 -0
- data/lib/taro/rails/response_validator.rb +53 -52
- data/lib/taro/rails/route_finder.rb +5 -7
- data/lib/taro/rails/tasks/export.rake +10 -9
- data/lib/taro/rails.rb +2 -3
- data/lib/taro/return_def.rb +43 -0
- data/lib/taro/route.rb +32 -0
- data/lib/taro/status_code.rb +16 -0
- data/lib/taro/types/base_type.rb +7 -1
- data/lib/taro/types/coercion.rb +2 -2
- data/lib/taro/types/enum_type.rb +1 -1
- data/lib/taro/types/field.rb +17 -5
- data/lib/taro/types/field_def.rb +62 -0
- data/lib/taro/types/field_validation.rb +4 -6
- data/lib/taro/types/input_type.rb +4 -9
- data/lib/taro/types/list_type.rb +1 -1
- data/lib/taro/types/nested_response_type.rb +16 -0
- data/lib/taro/types/object_type.rb +2 -7
- data/lib/taro/types/object_types/no_content_type.rb +1 -5
- data/lib/taro/types/object_types/page_info_type.rb +1 -1
- data/lib/taro/types/object_types/page_type.rb +1 -5
- data/lib/taro/types/response_type.rb +8 -0
- data/lib/taro/types/scalar/integer_param_type.rb +15 -0
- data/lib/taro/types/scalar_type.rb +1 -1
- data/lib/taro/types/shared/caching.rb +30 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
- data/lib/taro/types/shared/derived_types.rb +34 -15
- data/lib/taro/types/shared/equivalence.rb +14 -0
- data/lib/taro/types/shared/errors.rb +8 -8
- data/lib/taro/types/shared/fields.rb +10 -36
- data/lib/taro/types/shared/name.rb +14 -0
- data/lib/taro/types/shared/object_coercion.rb +0 -13
- data/lib/taro/types/shared/openapi_name.rb +0 -6
- data/lib/taro/types/shared/rendering.rb +5 -3
- data/lib/taro/types/shared.rb +1 -1
- data/lib/taro/types.rb +1 -1
- data/lib/taro/version.rb +1 -1
- data/lib/taro.rb +6 -1
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4f35df13466f65caaf120945539ca21fcd2562dc3e79145817c66f75f58c016
|
4
|
+
data.tar.gz: c3f2af6555b7a3c9f6e81a6ba69f5fceea0663ed6b93c22cfe584f478a2a4b24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3c7de4bb79a0f78f1060ea32bf65e1da9bf597f09790d95f5c0e09a0fdc175de973aebdc28c39a1aa994dbd0fd41e6be3097508ddfad57474ad774603aac933
|
7
|
+
data.tar.gz: aad75b1177ff07d1e95415e720076d85734de7ebd185e2503e4fa80fdf8ffba4e2f2cf4df735582ed6e69599cfba413ce901abcd043f178efcf1ec6ef9072054
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,40 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [2.1.0] - 2025-02-21
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- added support for caching on type level and when calling render
|
8
|
+
- default cache will be set to Rails.cache in the railtie
|
9
|
+
- cache_key can be a string, an array, a hash or a proc
|
10
|
+
|
11
|
+
## [2.0.0] - 2024-12-15
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
|
15
|
+
- rendering undeclared http error codes (except 422) is now allowed
|
16
|
+
- this previously raised errors when done in endpoints with other declarations
|
17
|
+
- as a result, some errors rendered from `rescue_from` blocks became 500s
|
18
|
+
- deduplicated response schemas for ad-hoc nested returns in OpenAPI export
|
19
|
+
- this only affects nested returns e.g. `returns :x, code: :ok, type: 'YType'`
|
20
|
+
- old name: `get_show_ys_200_Response`, new_name: `Y_in_x_Response`
|
21
|
+
- removed option to render nested returns with string keys
|
22
|
+
- e.g. for `returns :foo, [...]`, `render json: { 'foo' => [...] }` fails now
|
23
|
+
- removed `Taro::Rails.declarations`, replaced it with `Taro.declarations`
|
24
|
+
|
25
|
+
### Added
|
26
|
+
|
27
|
+
- added `::common_return` to define common return types
|
28
|
+
- added support for declaring path & query params as Integer
|
29
|
+
- e.g. `param :id, type: 'Integer', required: true` for `/users/1`
|
30
|
+
- e.g. `param :page, type: 'Integer', required: true` for `?page=1`
|
31
|
+
- added parsed/rendered object to validation errors for debugging
|
32
|
+
- improved validation error messages
|
33
|
+
|
34
|
+
### Fixed
|
35
|
+
|
36
|
+
- fixed unnecessary `$LOAD_PATH` searches at require time
|
37
|
+
|
3
38
|
## [1.4.0] - 2024-11-27
|
4
39
|
|
5
40
|
### Added
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Taro - Typed Api using Ruby Objects
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/taro)
|
4
|
+
[](https://github.com/taro-rb/taro/actions)
|
5
|
+
|
3
6
|
This library provides an object-based type system for RESTful Ruby APIs, with built-in parameter parsing, response rendering, and OpenAPI schema export.
|
4
7
|
|
5
8
|
It is inspired by [`apipie-rails`](https://github.com/Apipie/apipie-rails) and [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby).
|
@@ -32,25 +35,31 @@ This is how type classes can be used in a Rails controller:
|
|
32
35
|
class BikesController < ApplicationController
|
33
36
|
# This adds an endpoint summary, description, and tags to the docs (all optional)
|
34
37
|
api 'Update a bike', desc: 'My longer text', tags: ['Bikes']
|
35
|
-
|
38
|
+
|
39
|
+
# Params can come from the path, e.g. /bike/:id.
|
40
|
+
# Some types, like UUID in this case, are predefined. See below for more types.
|
36
41
|
param :id, type: 'UUID', null: false, desc: 'ID of the bike to update'
|
37
|
-
|
42
|
+
|
43
|
+
# Params can also come from the query string or request body.
|
44
|
+
# This describes a Hash param:
|
38
45
|
param :bike, type: 'BikeInputType', null: false
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
46
|
+
|
47
|
+
# Return types can differ by status code:
|
48
|
+
returns code: :ok, type: 'BikeType', desc: 'update success'
|
49
|
+
|
50
|
+
# Return types can also be nested (in this case in an "error" key):
|
51
|
+
returns :error, code: :unprocessable_content, type: 'MyErrorType'
|
52
|
+
|
43
53
|
def update
|
44
|
-
#
|
54
|
+
# Declared params are available as @api_params
|
45
55
|
bike = Bike.find(@api_params[:id])
|
46
56
|
success = bike.update(@api_params[:bike])
|
47
57
|
|
48
|
-
# Types
|
49
|
-
# The object
|
58
|
+
# Types are also used to render responses.
|
50
59
|
if success
|
51
|
-
render json:
|
60
|
+
render json: BikeType.render(bike), status: :ok
|
52
61
|
else
|
53
|
-
render json: MyErrorType.render(bike.errors.first), status: :unprocessable_entity
|
62
|
+
render json: { error: MyErrorType.render(bike.errors.first) }, status: :unprocessable_entity
|
54
63
|
end
|
55
64
|
end
|
56
65
|
|
@@ -63,18 +72,18 @@ class BikesController < ApplicationController
|
|
63
72
|
end
|
64
73
|
```
|
65
74
|
|
66
|
-
Notice the multiple roles of types: They are used to
|
75
|
+
Notice the multiple roles of types: They are used to describe the structure of API requests and responses, and render the response. Both the input and output of the API are validated against the schema by default (see below).
|
67
76
|
|
68
|
-
Here is an example of the `BikeType` from
|
77
|
+
Here is an example of the `BikeType` from the controller above:
|
69
78
|
|
70
79
|
```ruby
|
71
80
|
class BikeType < ObjectType
|
72
|
-
# Optional description of BikeType (for
|
81
|
+
# Optional description of BikeType (for the OpenAPI export)
|
73
82
|
self.desc = 'A bike and all relevant information about it'
|
74
83
|
|
75
84
|
# Object types have fields. Each field has a name, its own type,
|
76
85
|
# and a `null:` setting to indicate if it can be nil.
|
77
|
-
# Providing a
|
86
|
+
# Providing a description is optional.
|
78
87
|
field :brand, type: 'String', null: true, desc: 'The brand name'
|
79
88
|
|
80
89
|
# Fields can reference other types and arrays of values
|
@@ -95,7 +104,9 @@ class BikeType < ObjectType
|
|
95
104
|
end
|
96
105
|
```
|
97
106
|
|
98
|
-
### Input types
|
107
|
+
### Input and response types
|
108
|
+
|
109
|
+
You can use object types for input and output. However, if you want added control, you can also define dedicated input and response types.
|
99
110
|
|
100
111
|
Note the use of `BikeInputType` in the `param` declaration above? It could look like so:
|
101
112
|
|
@@ -106,7 +117,18 @@ class BikeInputType < InputType
|
|
106
117
|
end
|
107
118
|
```
|
108
119
|
|
109
|
-
|
120
|
+
A type inheriting from InputType behaves just like an ObjectType when parsing parameters. The only difference is that it can't be used in response declarations.
|
121
|
+
|
122
|
+
Likewise, there is a special type for responses, which can't be used in input declarations:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class BikeSearchResponseType < ResponseType
|
126
|
+
field :bike, type: 'BikeType', null: true, desc: 'The found bike'
|
127
|
+
field :search_duration, type: 'Integer', null: false
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
In cases where users may edit every attribute that you render, using a single ObjectType for both input and output is more convenient. E.g. `BikeType` could be used for both input and output in the `update` action above – if all its fields should be editable.
|
110
132
|
|
111
133
|
### Validation
|
112
134
|
|
@@ -120,15 +142,69 @@ Taro.config.parse_params = false
|
|
120
142
|
|
121
143
|
#### Response validation
|
122
144
|
|
123
|
-
Responses are automatically validated to
|
145
|
+
Responses are automatically validated to have used the correct type for rendering, which guarantees that they match the declaration. This means you have to use the types to render complex responses, and manually building a Hash that conforms to the schema will raise an error. Primitive/scalar types are an exception, e.g. you *can* do:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
returns code: :ok, array_of: 'String', desc: 'Bike names'
|
149
|
+
def index
|
150
|
+
render json: ['Super bike', 'Slow bike']
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
An error is also raised if a documented endpoint renders an undocumented status code, e.g.:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
returns code: :ok, type: 'BikeType'
|
158
|
+
def create
|
159
|
+
render json: BikeType.render(bike), status: :created
|
160
|
+
# => undeclared 201 response, raises response validation error
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
HTTP error codes, except 422, are an exception. They can be rendered without prior declaration. E.g.:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
returns code: :ok, type: 'BikeType'
|
168
|
+
def show
|
169
|
+
render json: something, status: :not_found # works
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
The reason for this is that Rack and Rails commonly render codes like 400, 404, 500 and various others, but you might not want to have that fact documented on every single endpoint.
|
174
|
+
|
175
|
+
However, if you do declare an error code, responses with this code *are* validated:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
returns code: :not_found, type: 'ExpectedErrorType'
|
179
|
+
def show
|
180
|
+
render json: WrongErrorType.render(foo), status: :not_found
|
181
|
+
# => type mismatch, raises response validation error
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
Response validation can be disabled:
|
124
186
|
|
125
187
|
```ruby
|
126
188
|
Taro.config.validate_responses = false
|
127
189
|
```
|
128
190
|
|
191
|
+
### Common error declarations
|
192
|
+
|
193
|
+
`::common_return` can be used to add a return declaration to all actions in a controller and its subclasses, and all related OpenAPI exports.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
class AuthenticatedApiBaseController < ApiBaseController
|
197
|
+
common_return code: :unauthorized, type: 'MyErrorType', desc: 'Log in first'
|
198
|
+
|
199
|
+
rescue_from 'MyAuthError' do
|
200
|
+
render json: MyErrorType.render(something), status: :unauthorized
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
129
205
|
### Included type options
|
130
206
|
|
131
|
-
The following type names are available by default and can be used as `type
|
207
|
+
The following type names are available by default and can be used as `type:`, `array_of:`, or `page_of:` arguments:
|
132
208
|
|
133
209
|
- `'Boolean'` - accepts and renders `true` or `false`
|
134
210
|
- `'Date'` - accepts and renders a date string in ISO8601 format
|
@@ -142,7 +218,7 @@ The following type names are available by default and can be used as `type:`/`ar
|
|
142
218
|
- `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
|
143
219
|
- `'UUID'` - accepts and renders UUIDs
|
144
220
|
|
145
|
-
Also, when using the generator, `ErrorsType` and `ErrorDetailsType` are generated as a starting point for unified error presentation. `ErrorsType` can render invalid `ActiveRecord` instances, `ActiveModel::Errors` and other data structures.
|
221
|
+
Also, when using the rails generator, `ErrorsType` and `ErrorDetailsType` are generated as a starting point for unified error presentation. `ErrorsType` can render invalid `ActiveRecord` instances, `ActiveModel::Errors` and other data structures.
|
146
222
|
|
147
223
|
### Enums
|
148
224
|
|
@@ -168,38 +244,13 @@ class ErrorType < ObjectType
|
|
168
244
|
end
|
169
245
|
```
|
170
246
|
|
171
|
-
|
172
|
-
|
173
|
-
#### How do I avoid repeating common error declarations?
|
174
|
-
|
175
|
-
Hook into the DSL in your base controller(s):
|
176
|
-
|
177
|
-
```ruby
|
178
|
-
class ApiBaseController < ApplicationController
|
179
|
-
def self.api(...)
|
180
|
-
super
|
181
|
-
returns code: :not_found, type: 'MyErrorType', desc: 'The record was not found'
|
182
|
-
end
|
247
|
+
## FAQ
|
183
248
|
|
184
|
-
|
249
|
+
### How do I render API docs?
|
185
250
|
|
186
|
-
|
187
|
-
render json: MyErrorType.render(something), status: :not_found
|
188
|
-
end
|
189
|
-
end
|
190
|
-
```
|
251
|
+
Rendering docs is outside of the scope of this project. You can use the OpenAPI export to generate docs with tools such as RapiDoc, ReDoc, Swagger UI, and [many others](https://tools.openapis.org/categories/documentation.html).
|
191
252
|
|
192
|
-
|
193
|
-
class AuthenticatedApiController < ApiBaseController
|
194
|
-
def self.api(...)
|
195
|
-
super
|
196
|
-
returns code: :unauthorized, type: 'MyErrorType'
|
197
|
-
end
|
198
|
-
# ... rescue_from ... render ...
|
199
|
-
end
|
200
|
-
```
|
201
|
-
|
202
|
-
#### How do I use context in my types?
|
253
|
+
### How do I use context in my types?
|
203
254
|
|
204
255
|
Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
|
205
256
|
|
@@ -213,18 +264,23 @@ class BikeType < ObjectType
|
|
213
264
|
end
|
214
265
|
```
|
215
266
|
|
216
|
-
|
267
|
+
### How do I migrate from apipie-rails?
|
268
|
+
|
269
|
+
First of all, if you don't need a better OpenAPI export, or better support for hashes and arrays, or less repetitive definitions, it might not be worth it.
|
270
|
+
|
271
|
+
Please also note the following:
|
217
272
|
|
218
|
-
|
273
|
+
- `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
|
274
|
+
- `taro` does not support arbitrary validations - only those that can be expressed in the OpenAPI schema
|
275
|
+
- `taro` does not render API docs, as mentioned above
|
219
276
|
|
220
|
-
If you
|
277
|
+
If you want to migrate anyway:
|
221
278
|
|
222
|
-
- note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
|
223
279
|
- extract complex param declarations into InputTypes
|
224
|
-
- extract complex response declarations into ObjectTypes
|
280
|
+
- extract complex response declarations into ObjectTypes or ResponseTypes
|
225
281
|
- replace `required: true` with `null: false` and `required: false` with `null: true`
|
226
282
|
|
227
|
-
|
283
|
+
Taro uses some of the same DSL as `apipie`, so for a step-by-step migration, you might want to make `taro` use a different one. This initializer will change the `taro` DSL to `taro_api`, `taro_param`, and `taro_returns` and leave `api`, `param`, and `returns` to `apipie`:
|
228
284
|
|
229
285
|
```ruby
|
230
286
|
# config/initializers/taro.rb
|
@@ -234,7 +290,7 @@ For a step-by-step migration, you might want to make `taro` use a different DSL
|
|
234
290
|
end
|
235
291
|
```
|
236
292
|
|
237
|
-
|
293
|
+
### How do I keep lengthy API descriptions out of my controller?
|
238
294
|
|
239
295
|
```ruby
|
240
296
|
module BikeUpdateDesc
|
@@ -252,13 +308,13 @@ class BikesController < ApplicationController
|
|
252
308
|
end
|
253
309
|
```
|
254
310
|
|
255
|
-
|
311
|
+
### Why do I have to use type name strings instead of the type constants?
|
256
312
|
|
257
313
|
Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
|
258
314
|
|
259
315
|
The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
|
260
316
|
|
261
|
-
|
317
|
+
### Can I define my own derived types like `page_of` or `array_of`?
|
262
318
|
|
263
319
|
Yes.
|
264
320
|
|
@@ -288,15 +344,37 @@ class MyController < ApplicationController
|
|
288
344
|
end
|
289
345
|
```
|
290
346
|
|
347
|
+
## Caching
|
348
|
+
|
349
|
+
Taro provides support for caching. The cache instance can be configured by setting `Taro::Cache.cache_instance`. By default, the railtie will set it to `Rails.cache`.
|
350
|
+
|
351
|
+
It supports configuring an add hoc cache when using a render call, e.g.
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
bike = Bike.find(params[:id])
|
355
|
+
BikeType.with_cache(cache_key: bike.cache_key, expires_in: 3.minutes).render(bike)
|
356
|
+
```
|
357
|
+
|
358
|
+
Or by configuring the cache rule on a per type bases, e.g.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
class BikeType < ObjectType
|
362
|
+
# Optional description of BikeType (for the OpenAPI export)
|
363
|
+
self.desc = 'A bike and all relevant information about it'
|
364
|
+
self.cache_key = ->(bike) { bike.cache_key_with_version }
|
365
|
+
self.expires_in = 1.hour
|
366
|
+
|
367
|
+
...
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
291
371
|
## Possible future features
|
292
372
|
|
293
373
|
- warning/raising for undeclared input params (currently they are ignored)
|
294
374
|
- usage without rails is possible but not convenient yet
|
295
375
|
- rspec matchers for testing
|
296
376
|
- sum types
|
297
|
-
- api doc rendering based on export (e.g. rails engine with web ui)
|
298
377
|
- [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
|
299
|
-
- maybe make `type:` optional for path params as they're always strings anyway
|
300
378
|
- various openapi features
|
301
379
|
- non-JSON content types (e.g. for file uploads)
|
302
380
|
- [examples](https://swagger.io/specification/#example-object)
|
data/lib/taro/cache.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Taro::Cache
|
2
|
+
singleton_class.attr_accessor :cache_instance
|
3
|
+
|
4
|
+
def self.call(object, cache_key: nil, expires_in: nil)
|
5
|
+
case cache_key
|
6
|
+
when nil
|
7
|
+
yield
|
8
|
+
when Hash, Proc
|
9
|
+
call(object, cache_key: cache_key[object], expires_in: expires_in) { yield }
|
10
|
+
else
|
11
|
+
cache_instance.fetch(cache_key, expires_in: expires_in) { yield }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Holds common return definitions for a set of declarations,
|
2
|
+
# e.g. shared error responses, within a class and its subclasses.
|
3
|
+
module Taro::CommonReturns
|
4
|
+
class << self
|
5
|
+
def define(klass, nesting = nil, **)
|
6
|
+
(map[klass] ||= []) << Taro::ReturnDef.new(nesting:, **)
|
7
|
+
klass.extend(InheritedCallback)
|
8
|
+
end
|
9
|
+
|
10
|
+
def inherit(from_class, to_class)
|
11
|
+
map[to_class] = map[from_class].dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def for(klass)
|
15
|
+
map[klass] || []
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def map
|
21
|
+
@map ||= {}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InheritedCallback
|
26
|
+
def inherited(new_class)
|
27
|
+
Taro::CommonReturns.inherit(self, new_class)
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Framework-agnostic, abstract class.
|
2
|
+
# Descendants must implement #endpoint and (only for openapi export) #routes.
|
3
|
+
# See Taro::Rails::Declaration for an example.
|
4
|
+
class Taro::Declaration
|
5
|
+
attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :tags
|
6
|
+
|
7
|
+
def initialize(for_klass = nil)
|
8
|
+
@params = Class.new(Taro::Types::InputType)
|
9
|
+
@return_defs = {}
|
10
|
+
@return_descriptions = {}
|
11
|
+
|
12
|
+
Taro::CommonReturns.for(for_klass).each { |rd| add_return_def(rd) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_info(summary, desc: nil, tags: nil)
|
16
|
+
summary.is_a?(String) || raise(Taro::ArgumentError, 'api summary must be a String')
|
17
|
+
@summary = summary
|
18
|
+
@desc = desc
|
19
|
+
@tags = Array(tags) if tags
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_param(param_name, **attributes)
|
23
|
+
if attributes[:type] == 'Integer'
|
24
|
+
attributes[:type] = 'Taro::Types::Scalar::IntegerParamType'
|
25
|
+
end
|
26
|
+
@params.field(param_name, **attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_return(nesting = nil, **)
|
30
|
+
return_def = Taro::ReturnDef.new(nesting:, **)
|
31
|
+
add_return_def(return_def)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return types are evaluated lazily to avoid unnecessary autoloading
|
35
|
+
# of all types in dev/test envs.
|
36
|
+
def returns
|
37
|
+
@returns ||= evaluate_return_defs
|
38
|
+
end
|
39
|
+
|
40
|
+
def routes
|
41
|
+
raise NotImplementedError, "implement ##{__method__} in subclass"
|
42
|
+
end
|
43
|
+
|
44
|
+
def endpoint
|
45
|
+
raise NotImplementedError, "implement ##{__method__} in subclass"
|
46
|
+
end
|
47
|
+
|
48
|
+
def polymorphic_route?
|
49
|
+
routes.size > 1
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect
|
53
|
+
"#<#{self.class} (#{endpoint || 'not finalized'})>"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def add_return_def(return_def)
|
59
|
+
raise_if_already_declared(return_def.code)
|
60
|
+
|
61
|
+
return_defs[return_def.code] = return_def
|
62
|
+
return_descriptions[return_def.code] = return_def.desc
|
63
|
+
end
|
64
|
+
|
65
|
+
def raise_if_already_declared(code)
|
66
|
+
(prev = return_defs[code]) && raise(Taro::ArgumentError, <<~MSG)
|
67
|
+
response for status #{code} already declared at #{prev.defined_at}
|
68
|
+
MSG
|
69
|
+
end
|
70
|
+
|
71
|
+
def evaluate_return_defs
|
72
|
+
return_defs.transform_values do |rd|
|
73
|
+
type = rd.evaluate
|
74
|
+
type.define_name("ResponseType(#{endpoint})") if rd.nesting
|
75
|
+
type
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def <=>(other)
|
80
|
+
routes.first.openapi_operation_id <=> other.routes.first.openapi_operation_id
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Taro
|
2
|
+
def self.declarations
|
3
|
+
DeclarationsMap
|
4
|
+
end
|
5
|
+
|
6
|
+
module DeclarationsMap
|
7
|
+
class << self
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
map[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(key, declaration)
|
15
|
+
map.key?(key) && raise(Taro::InvariantError, "#{key} already declared")
|
16
|
+
map[key] = declaration
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&)
|
20
|
+
map.each_value(&)
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset
|
24
|
+
map.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def map
|
30
|
+
@map ||= {}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/taro/errors.rb
CHANGED
@@ -1,12 +1,25 @@
|
|
1
1
|
class Taro::Error < StandardError
|
2
2
|
def message
|
3
3
|
# clean up newlines introduced when setting the message with a heredoc
|
4
|
-
super.chomp.
|
4
|
+
super.chomp.tr("\n", ' ')
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
8
|
class Taro::ArgumentError < Taro::Error; end
|
9
9
|
class Taro::RuntimeError < Taro::Error; end
|
10
|
-
class Taro::
|
10
|
+
class Taro::InvariantError < Taro::RuntimeError; end
|
11
|
+
|
12
|
+
class Taro::ValidationError < Taro::RuntimeError
|
13
|
+
attr_reader :object, :origin
|
14
|
+
|
15
|
+
def initialize(message, object, origin)
|
16
|
+
raise 'Abstract class' if instance_of?(Taro::ValidationError)
|
17
|
+
|
18
|
+
super(message)
|
19
|
+
@object = object
|
20
|
+
@origin = origin
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
11
24
|
class Taro::InputError < Taro::ValidationError; end
|
12
25
|
class Taro::ResponseError < Taro::ValidationError; end
|
data/lib/taro/export/base.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Taro::Export::Base
|
2
2
|
attr_reader :result
|
3
3
|
|
4
|
-
def self.call(declarations
|
4
|
+
def self.call(declarations: Taro.declarations, title: Taro.config.api_name, version: Taro.config.api_version, **)
|
5
5
|
new.call(declarations:, title:, version:, **)
|
6
6
|
end
|
7
7
|
|
@@ -6,8 +6,6 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
6
6
|
@schemas = {}
|
7
7
|
end
|
8
8
|
|
9
|
-
# TODO:
|
10
|
-
# - use json-schema gem to validate overall result against OpenAPIv3 schema
|
11
9
|
def call(declarations:, title:, version:)
|
12
10
|
@result = { openapi: '3.1.0', info: { title:, version: } }
|
13
11
|
paths = export_paths(declarations)
|
@@ -34,7 +32,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
34
32
|
operationId: route.openapi_operation_id,
|
35
33
|
parameters: route_parameters(declaration, route),
|
36
34
|
requestBody: request_body(declaration, route),
|
37
|
-
responses: responses(declaration
|
35
|
+
responses: responses(declaration),
|
38
36
|
}.compact,
|
39
37
|
}
|
40
38
|
end
|
@@ -45,9 +43,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
45
43
|
|
46
44
|
def path_parameters(declaration, route)
|
47
45
|
route.path_params.map do |param_name|
|
48
|
-
param_field = declaration.params.fields[param_name] || raise(
|
49
|
-
|
50
|
-
|
46
|
+
param_field = declaration.params.fields[param_name] || raise(
|
47
|
+
Taro::InvariantError,
|
48
|
+
"Declaration missing for path param #{param_name} of route #{route}"
|
49
|
+
)
|
51
50
|
|
52
51
|
# path params are always required in rails
|
53
52
|
export_parameter(param_field).merge(in: 'path', required: true)
|
@@ -77,8 +76,11 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
77
76
|
end
|
78
77
|
|
79
78
|
def validate_path_or_query_parameter(field)
|
80
|
-
[
|
81
|
-
|
79
|
+
ok = %i[string integer]
|
80
|
+
ok.include?(field.type.openapi_type) || raise(Taro::ArgumentError, <<~MSG)
|
81
|
+
Unsupported #{field.openapi_type} as path/query param "#{field.name}",
|
82
|
+
expected one of: #{ok.join(', ')}
|
83
|
+
MSG
|
82
84
|
end
|
83
85
|
|
84
86
|
def request_body(declaration, route)
|
@@ -110,28 +112,21 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
110
112
|
end
|
111
113
|
end
|
112
114
|
|
113
|
-
def responses(declaration
|
114
|
-
name_anonymous_return_types(declaration, route)
|
115
|
-
|
115
|
+
def responses(declaration)
|
116
116
|
declaration.returns.sort.to_h do |code, type|
|
117
|
+
# response description is required in openapi 3 – fall back to status code
|
118
|
+
description = declaration.return_descriptions[code] || type.desc ||
|
119
|
+
Taro::StatusCode.coerce_to_message(code)
|
117
120
|
[
|
118
121
|
code.to_s,
|
119
122
|
{
|
120
|
-
description
|
123
|
+
description:,
|
121
124
|
content: { 'application/json': { schema: export_type(type) } },
|
122
125
|
}
|
123
126
|
]
|
124
127
|
end
|
125
128
|
end
|
126
129
|
|
127
|
-
def name_anonymous_return_types(declaration, route)
|
128
|
-
declaration.returns.each do |code, type|
|
129
|
-
next if type.openapi_name?
|
130
|
-
|
131
|
-
type.openapi_name = "#{route.openapi_operation_id}_#{code}_Response"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
130
|
def export_type(type)
|
136
131
|
if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
|
137
132
|
{ type: type.openapi_type }
|
@@ -199,7 +194,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
199
194
|
elsif custom_scalar_type?(type)
|
200
195
|
custom_scalar_type_details(type)
|
201
196
|
else
|
202
|
-
raise
|
197
|
+
raise Taro::InvariantError, "Unexpected type: #{type}"
|
203
198
|
end
|
204
199
|
end
|
205
200
|
|
@@ -244,8 +239,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
244
239
|
|
245
240
|
def assert_unique_openapi_name(type)
|
246
241
|
@name_to_type_map ||= {}
|
247
|
-
if (prev = @name_to_type_map[type.openapi_name]) && type
|
248
|
-
raise
|
242
|
+
if (prev = @name_to_type_map[type.openapi_name]) && !prev.equivalent?(type)
|
243
|
+
raise Taro::InvariantError, <<~MSG
|
244
|
+
Duplicate openapi_name "#{type.openapi_name}" for types #{prev} and #{type}
|
245
|
+
MSG
|
249
246
|
else
|
250
247
|
@name_to_type_map[type.openapi_name] = type
|
251
248
|
end
|