taro 1.4.0 → 2.0.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 +27 -0
- data/README.md +89 -50
- 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/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/response_validator.rb +51 -50
- data/lib/taro/rails/route_finder.rb +1 -1
- 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 +6 -1
- data/lib/taro/types/field.rb +16 -4
- 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/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/custom_field_resolvers.rb +2 -2
- data/lib/taro/types/shared/derived_types.rb +34 -15
- data/lib/taro/types/shared/equivalence.rb +15 -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.rb +1 -1
- data/lib/taro/types.rb +1 -1
- data/lib/taro/version.rb +1 -1
- data/lib/taro.rb +6 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62127cdef8f44e34831cb4dec7d900bfa25c3c00994e750624de1f486c6880ab
|
4
|
+
data.tar.gz: 40bd31112d3e8fb3fe96bfa282acd6ad909c60f66431aa556e01052b3098ab44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a609041860bc3516a70a1ebd2f073d031cac39a04f9ff5562ff7c6d0711b1c5dc3a900cfa4842ff77f6ab573ab0fa2164282cf68fff58133071a7b842201cecc
|
7
|
+
data.tar.gz: 4942e27cdc067cb5c46ae7f726743d1cf54282536195b892d93507757820e0c53db46ebfb5a40ad25cc13c60ef2eac6b493cb1e2d8b9cfa195a0db782734d765
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [2.0.0] - 2024-12-15
|
4
|
+
|
5
|
+
### Changed
|
6
|
+
|
7
|
+
- rendering undeclared http error codes (except 422) is now allowed
|
8
|
+
- this previously raised errors when done in endpoints with other declarations
|
9
|
+
- as a result, some errors rendered from `rescue_from` blocks became 500s
|
10
|
+
- deduplicated response schemas for ad-hoc nested returns in OpenAPI export
|
11
|
+
- this only affects nested returns e.g. `returns :x, code: :ok, type: 'YType'`
|
12
|
+
- old name: `get_show_ys_200_Response`, new_name: `Y_in_x_Response`
|
13
|
+
- removed option to render nested returns with string keys
|
14
|
+
- e.g. for `returns :foo, [...]`, `render json: { 'foo' => [...] }` fails now
|
15
|
+
- removed `Taro::Rails.declarations`, replaced it with `Taro.declarations`
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- added `::common_return` to define common return types
|
20
|
+
- added support for declaring path & query params as Integer
|
21
|
+
- e.g. `param :id, type: 'Integer', required: true` for `/users/1`
|
22
|
+
- e.g. `param :page, type: 'Integer', required: true` for `?page=1`
|
23
|
+
- added parsed/rendered object to validation errors for debugging
|
24
|
+
- improved validation error messages
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
- fixed unnecessary `$LOAD_PATH` searches at require time
|
29
|
+
|
3
30
|
## [1.4.0] - 2024-11-27
|
4
31
|
|
5
32
|
### Added
|
data/README.md
CHANGED
@@ -32,25 +32,31 @@ This is how type classes can be used in a Rails controller:
|
|
32
32
|
class BikesController < ApplicationController
|
33
33
|
# This adds an endpoint summary, description, and tags to the docs (all optional)
|
34
34
|
api 'Update a bike', desc: 'My longer text', tags: ['Bikes']
|
35
|
-
|
35
|
+
|
36
|
+
# Params can come from the path, e.g. /bike/:id.
|
37
|
+
# Some types, like UUID in this case, are predefined. See below for more.
|
36
38
|
param :id, type: 'UUID', null: false, desc: 'ID of the bike to update'
|
37
|
-
|
39
|
+
|
40
|
+
# Params can also come from the query string or request body.
|
41
|
+
# This describes a Hash param:
|
38
42
|
param :bike, type: 'BikeInputType', null: false
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
|
44
|
+
# Return types can differ by status code:
|
45
|
+
returns code: :ok, type: 'BikeType', desc: 'update success'
|
46
|
+
|
47
|
+
# Return types can also be nested (in this case in an "error" key):
|
48
|
+
returns :error, code: :unprocessable_content, type: 'MyErrorType'
|
49
|
+
|
43
50
|
def update
|
44
|
-
#
|
51
|
+
# Declared params are available as @api_params
|
45
52
|
bike = Bike.find(@api_params[:id])
|
46
53
|
success = bike.update(@api_params[:bike])
|
47
54
|
|
48
|
-
# Types
|
49
|
-
# The object
|
55
|
+
# Types are also used to render responses.
|
50
56
|
if success
|
51
|
-
render json:
|
57
|
+
render json: BikeType.render(bike), status: :ok
|
52
58
|
else
|
53
|
-
render json: MyErrorType.render(bike.errors.first), status: :unprocessable_entity
|
59
|
+
render json: { error: MyErrorType.render(bike.errors.first) }, status: :unprocessable_entity
|
54
60
|
end
|
55
61
|
end
|
56
62
|
|
@@ -63,18 +69,18 @@ class BikesController < ApplicationController
|
|
63
69
|
end
|
64
70
|
```
|
65
71
|
|
66
|
-
Notice the multiple roles of types: They are used to
|
72
|
+
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
73
|
|
68
|
-
Here is an example of the `BikeType` from
|
74
|
+
Here is an example of the `BikeType` from the controller above:
|
69
75
|
|
70
76
|
```ruby
|
71
77
|
class BikeType < ObjectType
|
72
|
-
# Optional description of BikeType (for
|
78
|
+
# Optional description of BikeType (for the OpenAPI export)
|
73
79
|
self.desc = 'A bike and all relevant information about it'
|
74
80
|
|
75
81
|
# Object types have fields. Each field has a name, its own type,
|
76
82
|
# and a `null:` setting to indicate if it can be nil.
|
77
|
-
# Providing a
|
83
|
+
# Providing a description is optional.
|
78
84
|
field :brand, type: 'String', null: true, desc: 'The brand name'
|
79
85
|
|
80
86
|
# Fields can reference other types and arrays of values
|
@@ -95,7 +101,9 @@ class BikeType < ObjectType
|
|
95
101
|
end
|
96
102
|
```
|
97
103
|
|
98
|
-
### Input types
|
104
|
+
### Input and response types
|
105
|
+
|
106
|
+
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
107
|
|
100
108
|
Note the use of `BikeInputType` in the `param` declaration above? It could look like so:
|
101
109
|
|
@@ -106,7 +114,18 @@ class BikeInputType < InputType
|
|
106
114
|
end
|
107
115
|
```
|
108
116
|
|
109
|
-
|
117
|
+
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.
|
118
|
+
|
119
|
+
Likewise, there is a special type for responses, which can't be used in input declarations:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class BikeSearchResponseType < ResponseType
|
123
|
+
field :bike, type: 'BikeType', null: true, desc: 'The found bike'
|
124
|
+
field :search_duration, type: 'Integer', null: false
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
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
129
|
|
111
130
|
### Validation
|
112
131
|
|
@@ -120,12 +139,59 @@ Taro.config.parse_params = false
|
|
120
139
|
|
121
140
|
#### Response validation
|
122
141
|
|
123
|
-
Responses are automatically validated to
|
142
|
+
Responses are automatically validated to have used the correct type for rendering, which guarantees that they match the declaration.
|
143
|
+
|
144
|
+
An error is also raised if a documented endpoint renders an undocumented status code, e.g.:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
returns code: :ok, type: 'BikeType'
|
148
|
+
def create
|
149
|
+
render json: BikeType.render(bike), status: :created
|
150
|
+
# => undeclared 201 response, raises response validation error
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
However, all HTTP error codes except 422 can be rendered without prior declaration. E.g.:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
returns code: :ok, type: 'BikeType'
|
158
|
+
def show
|
159
|
+
render json: something, status: :not_found # works
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
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.
|
164
|
+
|
165
|
+
However, if you do declare an error code, responses with this code are validated:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
returns code: :not_found, type: 'ExpectedType'
|
169
|
+
def show
|
170
|
+
render json: WrongType.render(foo), status: :not_found
|
171
|
+
# => type mismatch, raises response validation error
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
Response validation can be disabled:
|
124
176
|
|
125
177
|
```ruby
|
126
178
|
Taro.config.validate_responses = false
|
127
179
|
```
|
128
180
|
|
181
|
+
### Common error declarations
|
182
|
+
|
183
|
+
`::common_return` can be used to add a return declaration to all actions in a controller and its subclasses, and all related OpenAPI exports.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class AuthenticatedApiBaseController < ApiBaseController
|
187
|
+
common_return code: :unauthorized, type: 'MyErrorType', desc: 'Log in first'
|
188
|
+
|
189
|
+
rescue_from 'MyAuthError' do
|
190
|
+
render json: MyErrorType.render(something), status: :unauthorized
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
129
195
|
### Included type options
|
130
196
|
|
131
197
|
The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
|
@@ -170,34 +236,9 @@ end
|
|
170
236
|
|
171
237
|
### FAQ
|
172
238
|
|
173
|
-
#### How do I
|
174
|
-
|
175
|
-
Hook into the DSL in your base controller(s):
|
239
|
+
#### How do I render API docs?
|
176
240
|
|
177
|
-
|
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
|
183
|
-
|
184
|
-
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
185
|
-
|
186
|
-
def render_not_found
|
187
|
-
render json: MyErrorType.render(something), status: :not_found
|
188
|
-
end
|
189
|
-
end
|
190
|
-
```
|
191
|
-
|
192
|
-
```ruby
|
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
|
-
```
|
241
|
+
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, or Swagger UI.
|
201
242
|
|
202
243
|
#### How do I use context in my types?
|
203
244
|
|
@@ -215,16 +256,16 @@ end
|
|
215
256
|
|
216
257
|
#### How do I migrate from apipie-rails?
|
217
258
|
|
218
|
-
First of all, if you don't need a better OpenAPI export, or better support for hashes and arrays, it might not be worth it.
|
259
|
+
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.
|
219
260
|
|
220
261
|
If you do:
|
221
262
|
|
222
263
|
- note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
|
223
264
|
- extract complex param declarations into InputTypes
|
224
|
-
- extract complex response declarations into ObjectTypes
|
265
|
+
- extract complex response declarations into ObjectTypes or ResponseTypes
|
225
266
|
- replace `required: true` with `null: false` and `required: false` with `null: true`
|
226
267
|
|
227
|
-
|
268
|
+
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:
|
228
269
|
|
229
270
|
```ruby
|
230
271
|
# config/initializers/taro.rb
|
@@ -294,9 +335,7 @@ end
|
|
294
335
|
- usage without rails is possible but not convenient yet
|
295
336
|
- rspec matchers for testing
|
296
337
|
- sum types
|
297
|
-
- api doc rendering based on export (e.g. rails engine with web ui)
|
298
338
|
- [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
339
|
- various openapi features
|
301
340
|
- non-JSON content types (e.g. for file uploads)
|
302
341
|
- [examples](https://swagger.io/specification/#example-object)
|
@@ -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
|
data/lib/taro/export.rb
CHANGED
data/lib/taro/none.rb
ADDED
@@ -1,19 +1,11 @@
|
|
1
1
|
module Taro::Rails::ActiveDeclarations
|
2
2
|
def apply(declaration:, controller_class:, action_name:)
|
3
|
-
|
3
|
+
Taro.declarations["#{controller_class.name}##{action_name}"] = declaration
|
4
4
|
Taro::Rails::ParamParsing.install(controller_class:, action_name:)
|
5
5
|
Taro::Rails::ResponseValidation.install(controller_class:)
|
6
6
|
end
|
7
7
|
|
8
|
-
def declarations_map
|
9
|
-
@declarations_map ||= {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def declarations
|
13
|
-
declarations_map.values.flat_map(&:values)
|
14
|
-
end
|
15
|
-
|
16
8
|
def declaration_for(controller)
|
17
|
-
|
9
|
+
Taro.declarations["#{controller.class.name}##{controller.action_name}"]
|
18
10
|
end
|
19
11
|
end
|