taro 1.0.0 → 1.2.0
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 +4 -4
- data/CHANGELOG.md +35 -1
- data/README.md +77 -14
- data/lib/taro/errors.rb +7 -1
- data/lib/taro/export/open_api_v3.rb +54 -23
- data/lib/taro/rails/active_declarations.rb +1 -1
- data/lib/taro/rails/declaration.rb +50 -9
- data/lib/taro/rails/generators/install_generator.rb +1 -1
- data/lib/taro/rails/generators/templates/errors_type.erb +15 -10
- data/lib/taro/rails/normalized_route.rb +8 -0
- data/lib/taro/rails/response_validation.rb +7 -57
- data/lib/taro/rails/response_validator.rb +109 -0
- data/lib/taro/rails/tasks/export.rake +5 -1
- data/lib/taro/rails.rb +1 -2
- data/lib/taro/types/base_type.rb +2 -0
- data/lib/taro/types/coercion.rb +28 -17
- data/lib/taro/types/enum_type.rb +2 -2
- data/lib/taro/types/field.rb +8 -16
- data/lib/taro/types/field_validation.rb +1 -1
- data/lib/taro/types/list_type.rb +4 -6
- data/lib/taro/types/object_types/free_form_type.rb +1 -0
- data/lib/taro/types/object_types/no_content_type.rb +1 -0
- data/lib/taro/types/object_types/page_info_type.rb +2 -0
- data/lib/taro/types/object_types/page_type.rb +15 -25
- data/lib/taro/types/scalar/iso8601_date_type.rb +1 -0
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +1 -0
- data/lib/taro/types/scalar/timestamp_type.rb +1 -0
- data/lib/taro/types/scalar/uuid_v4_type.rb +1 -0
- data/lib/taro/types/shared/deprecation.rb +3 -0
- data/lib/taro/types/shared/derived_types.rb +27 -0
- data/lib/taro/types/shared/errors.rb +3 -1
- data/lib/taro/types/shared/fields.rb +6 -5
- data/lib/taro/types/shared/item_type.rb +1 -0
- data/lib/taro/types/shared/object_coercion.rb +13 -0
- data/lib/taro/types/shared/openapi_name.rb +8 -6
- data/lib/taro/types/shared/rendering.rb +11 -25
- data/lib/taro/version.rb +1 -1
- data/tasks/benchmark.rake +1 -1
- metadata +7 -5
- data/lib/taro/types/shared/derivable_types.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5007f07dcb3230a1a45011138c19cc0f233e0a0e0fa81d64d554ee3d5cc4a82e
|
4
|
+
data.tar.gz: 99dde57ec71bb2724a29005e644a0bb23d642df11fe4555bd9203340169814df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: abb5fb481da01a4a50b19e891c3373b42372c97993788ed24c51b07146e087e25d040bc4ea077606a14013633b30f6254f6ced2e939ba9b039b341d95f918211
|
7
|
+
data.tar.gz: b5dd6c2222598ad3d8ee64c64fd0fc6c40299c3f071b08317d17aa29c3f786924ae4b064f587b1e548ba102e0cc802b8a5adf7ccbadf033795974bfdaae5329a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,39 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [1.2.0] - 2024-11-18
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Improved error messages
|
8
|
+
- Option to define custom derived types
|
9
|
+
- Option to use custom keys in paginated content
|
10
|
+
- Option to deprecate individual fields, params, and types
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
|
14
|
+
- Fixed nullable enum fields raising for null input
|
15
|
+
- Fixed auto-loading of return types
|
16
|
+
- Fixed console spam when inspecting declarations
|
17
|
+
- Fixed resolver method not being used when rendering a Hash
|
18
|
+
- Fixed the ErrorsType template
|
19
|
+
- Many fixes for OpenAPI export
|
20
|
+
- Fixed export of parameters for http methods without body
|
21
|
+
- Fixed export for PageType
|
22
|
+
- Fixed export for arrays of UUIDs, Dates, and Times
|
23
|
+
- Fixed export YML keys for namespaced controllers
|
24
|
+
- Reference plain types for repeated flat return types
|
25
|
+
- Made order of paths, verbs, responses and schemas deterministic
|
26
|
+
|
27
|
+
## [1.1.0] - 2024-11-16
|
28
|
+
|
29
|
+
### Added
|
30
|
+
|
31
|
+
- Response validation refined
|
32
|
+
|
33
|
+
### Fixed
|
34
|
+
|
35
|
+
- Bugfix for openapi export
|
36
|
+
|
37
|
+
## [1.0.0] - 2024-11-14
|
4
38
|
|
5
39
|
- Initial release
|
data/README.md
CHANGED
@@ -10,11 +10,6 @@ It is inspired by [`apipie-rails`](https://github.com/Apipie/apipie-rails) and [
|
|
10
10
|
- conveniently check request and response data against the declaration
|
11
11
|
- offer an up-to-date OpenAPI export with minimal configuration
|
12
12
|
|
13
|
-
## ⚠️ This is a work in progress - TODO:
|
14
|
-
|
15
|
-
- ISO8601Time, ISO8601Date types
|
16
|
-
- ResponseValidation: allow rendering scalars directly (e.g. `render json: 42`)
|
17
|
-
|
18
13
|
## Installation
|
19
14
|
|
20
15
|
```bash
|
@@ -136,16 +131,18 @@ Taro.config.validate_responses = false
|
|
136
131
|
The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
|
137
132
|
|
138
133
|
- `'Boolean'` - accepts and renders `true` or `false`
|
134
|
+
- `'Date'` - accepts and renders a date string in ISO8601 format
|
135
|
+
- `'DateTime'` - an alias for `'Time'`
|
139
136
|
- `'Float'`
|
140
137
|
- `'FreeForm'` - accepts and renders any JSON-serializable object, use with care
|
141
138
|
- `'Integer'`
|
142
|
-
- `'
|
139
|
+
- `'NoContent'` - renders an empty object, for use with `status: :no_content`
|
143
140
|
- `'String'`
|
144
|
-
- `'Timestamp'` - renders a `Time` as unix timestamp integer and turns into incoming integers into a `Time`
|
145
|
-
- `'UUID'` - accepts and renders UUIDs
|
146
|
-
- `'Date'` - accepts and renders a date string in ISO8601 format
|
147
141
|
- `'Time'` - accepts and renders a time string in ISO8601 format
|
148
|
-
- `'
|
142
|
+
- `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
|
143
|
+
- `'UUID'` - accepts and renders UUIDs
|
144
|
+
|
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.
|
149
146
|
|
150
147
|
### Enums
|
151
148
|
|
@@ -173,7 +170,7 @@ end
|
|
173
170
|
|
174
171
|
### FAQ
|
175
172
|
|
176
|
-
#### How
|
173
|
+
#### How do I avoid repeating common error declarations?
|
177
174
|
|
178
175
|
Hook into the DSL in your base controller(s):
|
179
176
|
|
@@ -202,7 +199,7 @@ class AuthenticatedApiController < ApiBaseController
|
|
202
199
|
end
|
203
200
|
```
|
204
201
|
|
205
|
-
#### How
|
202
|
+
#### How do I use context in my types?
|
206
203
|
|
207
204
|
Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
|
208
205
|
|
@@ -216,13 +213,80 @@ class BikeType < ObjectType
|
|
216
213
|
end
|
217
214
|
```
|
218
215
|
|
216
|
+
#### How do I migrate from apipie-rails?
|
217
|
+
|
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.
|
219
|
+
|
220
|
+
If you do:
|
221
|
+
|
222
|
+
- note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
|
223
|
+
- extract complex param declarations into InputTypes
|
224
|
+
- extract complex response declarations into ObjectTypes
|
225
|
+
- replace `required: true` with `null: false` and `required: false` with `null: true`
|
226
|
+
|
227
|
+
For a step-by-step migration, you might want to make `taro` use a different DSL then `apipie`:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
# config/initializers/taro.rb
|
231
|
+
%i[api param returns].each do |m|
|
232
|
+
Taro::Rails::DSL.alias_method("taro_#{m}", m) # `taro_api` etc.
|
233
|
+
Taro::Rails::DSL.define_method(m) { |*a, **k, &b| super(*a, **k, &b) }
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
#### How do I keep lengthy API descriptions out of my controller?
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
module BikeUpdateDesc
|
241
|
+
extend ActiveSupport::Concern
|
242
|
+
|
243
|
+
included do
|
244
|
+
api 'Update a bike', description: 'Long description', tags: ['Bikes']
|
245
|
+
# lots of params and returns ...
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class BikesController < ApplicationController
|
250
|
+
include BikeUpdateDesc
|
251
|
+
def update # ...
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
219
255
|
#### Why do I have to use type name strings instead of the type constants?
|
220
256
|
|
221
257
|
Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
|
222
258
|
|
223
259
|
The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
|
224
260
|
|
225
|
-
|
261
|
+
#### Can I define my own derived types like `page_of` or `array_of`?
|
262
|
+
|
263
|
+
Yes.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# Implement ::derive_from in your custom type.
|
267
|
+
class PreviewType < Taro::Types::Scalar::StringType
|
268
|
+
singleton_class.attr_reader :type_to_preview
|
269
|
+
|
270
|
+
def self.derive_from(other_type)
|
271
|
+
self.type_to_preview = other_type
|
272
|
+
end
|
273
|
+
|
274
|
+
def coerce_response
|
275
|
+
type_to_preview.new(object).coerce_response.to_s.truncate(100)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Make it available in the DSL, e.g. in an initializer.
|
280
|
+
Taro::Types::BaseType.define_derived_type :preview, 'PreviewType'
|
281
|
+
|
282
|
+
# Usage:
|
283
|
+
class MyController < ApplicationController
|
284
|
+
returns code: :ok, preview_of: 'BikeType'
|
285
|
+
def show
|
286
|
+
render json: BikeType.preview.render(Bike.find(params[:id]))
|
287
|
+
end
|
288
|
+
end
|
289
|
+
```
|
226
290
|
|
227
291
|
## Possible future features
|
228
292
|
|
@@ -232,7 +296,6 @@ This already works fo type classes – they don't trigger loading of referenced
|
|
232
296
|
- sum types
|
233
297
|
- api doc rendering based on export (e.g. rails engine with web ui)
|
234
298
|
- [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
|
235
|
-
- deprecation feature
|
236
299
|
- maybe make `type:` optional for path params as they're always strings anyway
|
237
300
|
- various openapi features
|
238
301
|
- non-JSON content types (e.g. for file uploads)
|
data/lib/taro/errors.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
class Taro::Error < StandardError
|
1
|
+
class Taro::Error < StandardError
|
2
|
+
def message
|
3
|
+
# clean up newlines introduced when setting the message with a heredoc
|
4
|
+
super.chomp.sub(/\n(?=\S)/, ' ')
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
2
8
|
class Taro::ArgumentError < Taro::Error; end
|
3
9
|
class Taro::RuntimeError < Taro::Error; end
|
4
10
|
class Taro::ValidationError < Taro::RuntimeError; end # not to be used directly
|
@@ -11,15 +11,16 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
11
11
|
def call(declarations:, title:, version:)
|
12
12
|
@result = { openapi: '3.1.0', info: { title:, version: } }
|
13
13
|
paths = export_paths(declarations)
|
14
|
-
@result[:paths] = paths if paths.any?
|
15
|
-
@result[:components] = { schemas: } if schemas.any?
|
14
|
+
@result[:paths] = paths.sort.to_h if paths.any?
|
15
|
+
@result[:components] = { schemas: schemas.sort.to_h } if schemas.any?
|
16
16
|
self
|
17
17
|
end
|
18
18
|
|
19
19
|
def export_paths(declarations)
|
20
|
-
declarations.each_with_object({}) do |declaration, paths|
|
20
|
+
declarations.sort.each_with_object({}) do |declaration, paths|
|
21
21
|
declaration.routes.each do |route|
|
22
|
-
paths[route.openapi_path]
|
22
|
+
paths[route.openapi_path] ||= {}
|
23
|
+
paths[route.openapi_path].merge! export_route(route, declaration)
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
@@ -30,30 +31,51 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
30
31
|
description: declaration.desc,
|
31
32
|
summary: declaration.summary,
|
32
33
|
tags: declaration.tags,
|
33
|
-
parameters:
|
34
|
+
parameters: route_parameters(declaration, route),
|
34
35
|
requestBody: request_body(declaration, route),
|
35
36
|
responses: responses(declaration),
|
36
37
|
}.compact,
|
37
38
|
}
|
38
39
|
end
|
39
40
|
|
41
|
+
def route_parameters(declaration, route)
|
42
|
+
path_parameters(declaration, route) + query_parameters(declaration, route)
|
43
|
+
end
|
44
|
+
|
40
45
|
def path_parameters(declaration, route)
|
41
46
|
route.path_params.map do |param_name|
|
42
47
|
param_field = declaration.params.fields[param_name] || raise(<<~MSG)
|
43
48
|
Declaration missing for path param #{param_name} of route #{route.endpoint}
|
44
49
|
MSG
|
45
50
|
|
46
|
-
|
47
|
-
|
48
|
-
in: 'path',
|
49
|
-
description: param_field.desc,
|
50
|
-
required: true, # path params are always required in rails
|
51
|
-
schema: { type: param_field.openapi_type },
|
52
|
-
}.compact
|
51
|
+
# path params are always required in rails
|
52
|
+
export_parameter(param_field).merge(in: 'path', required: true)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def query_parameters(declaration, route)
|
57
|
+
return [] if route.can_have_request_body?
|
58
|
+
|
59
|
+
declaration.params.fields.filter_map do |name, param_field|
|
60
|
+
next if route.path_params.include?(name)
|
61
|
+
|
62
|
+
export_parameter(param_field).merge(in: 'query')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def export_parameter(field)
|
67
|
+
{
|
68
|
+
name: field.name,
|
69
|
+
deprecated: field.deprecated,
|
70
|
+
description: field.desc,
|
71
|
+
required: !field.null,
|
72
|
+
schema: { type: field.openapi_type },
|
73
|
+
}.compact
|
74
|
+
end
|
75
|
+
|
56
76
|
def request_body(declaration, route)
|
77
|
+
return unless route.can_have_request_body?
|
78
|
+
|
57
79
|
params = declaration.params
|
58
80
|
body_param_fields = params.fields.reject do |name, _field|
|
59
81
|
route.path_params.include?(name)
|
@@ -81,7 +103,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
81
103
|
end
|
82
104
|
|
83
105
|
def responses(declaration)
|
84
|
-
declaration.returns.to_h do |code, type|
|
106
|
+
declaration.returns.sort.to_h do |code, type|
|
85
107
|
[
|
86
108
|
code.to_s,
|
87
109
|
{
|
@@ -114,23 +136,29 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
114
136
|
# as it puts props like format together with the main type.
|
115
137
|
# https://github.com/OAI/OpenAPI-Specification/issues/3148
|
116
138
|
base = { oneOf: [base, { type: 'null' }] } if field.null
|
117
|
-
base
|
118
|
-
base[:default] = field.default if field.default_specified?
|
119
|
-
base[:enum] = field.enum if field.enum
|
120
|
-
base
|
139
|
+
base.merge(field_metadata(field))
|
121
140
|
end
|
122
141
|
|
123
142
|
def export_complex_field_ref(field)
|
124
143
|
ref = extract_component_ref(field.type)
|
144
|
+
return ref if field_metadata(field).empty? && !field.null
|
145
|
+
|
125
146
|
if field.null
|
126
147
|
# RE nullable: https://stackoverflow.com/a/70658334
|
127
|
-
{
|
128
|
-
|
148
|
+
{ oneOf: [ref, { type: 'null' }] }
|
149
|
+
else # i.e. with metadata such as description or deprecated
|
129
150
|
# https://github.com/OAI/OpenAPI-Specification/issues/2033
|
130
|
-
{
|
131
|
-
|
132
|
-
|
133
|
-
|
151
|
+
{ allOf: [ref] }
|
152
|
+
end.merge(field_metadata(field))
|
153
|
+
end
|
154
|
+
|
155
|
+
def field_metadata(field)
|
156
|
+
meta = {}
|
157
|
+
meta[:description] = field.desc if field.desc
|
158
|
+
meta[:deprecated] = field.deprecated unless field.deprecated.nil?
|
159
|
+
meta[:default] = field.default if field.default_specified?
|
160
|
+
meta[:enum] = field.enum if field.enum
|
161
|
+
meta
|
134
162
|
end
|
135
163
|
|
136
164
|
def extract_component_ref(type)
|
@@ -155,6 +183,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
155
183
|
required = type.fields.values.reject(&:null).map(&:name)
|
156
184
|
{
|
157
185
|
type: type.openapi_type,
|
186
|
+
deprecated: type.deprecated,
|
158
187
|
description: type.desc,
|
159
188
|
required: (required if required.any?),
|
160
189
|
properties: type.fields.to_h { |name, f| [name, export_field(f)] },
|
@@ -165,6 +194,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
165
194
|
def enum_type_details(enum)
|
166
195
|
{
|
167
196
|
type: enum.item_type.openapi_type,
|
197
|
+
deprecated: enum.deprecated,
|
168
198
|
description: enum.desc,
|
169
199
|
enum: enum.values,
|
170
200
|
}.compact
|
@@ -173,6 +203,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
173
203
|
def list_type_details(list)
|
174
204
|
{
|
175
205
|
type: 'array',
|
206
|
+
deprecated: list.deprecated,
|
176
207
|
description: list.desc,
|
177
208
|
items: export_type(list.item_type),
|
178
209
|
}.compact
|
@@ -2,7 +2,7 @@ module Taro::Rails::ActiveDeclarations
|
|
2
2
|
def apply(declaration:, controller_class:, action_name:)
|
3
3
|
(declarations_map[controller_class] ||= {})[action_name] = declaration
|
4
4
|
Taro::Rails::ParamParsing.install(controller_class:, action_name:)
|
5
|
-
Taro::Rails::ResponseValidation.install(controller_class
|
5
|
+
Taro::Rails::ResponseValidation.install(controller_class:)
|
6
6
|
end
|
7
7
|
|
8
8
|
def declarations_map
|
@@ -1,9 +1,9 @@
|
|
1
1
|
class Taro::Rails::Declaration
|
2
|
-
attr_reader :desc, :summary, :params, :
|
2
|
+
attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :return_nestings, :routes, :tags
|
3
3
|
|
4
4
|
def initialize
|
5
5
|
@params = Class.new(Taro::Types::InputType)
|
6
|
-
@
|
6
|
+
@return_defs = {}
|
7
7
|
@return_descriptions = {}
|
8
8
|
@return_nestings = {}
|
9
9
|
end
|
@@ -24,8 +24,11 @@ class Taro::Rails::Declaration
|
|
24
24
|
status = self.class.coerce_status_to_int(code)
|
25
25
|
raise_if_already_declared(status)
|
26
26
|
|
27
|
+
kwargs[:nesting] = nesting
|
28
|
+
check_return_kwargs(kwargs)
|
29
|
+
|
27
30
|
kwargs[:defined_at] = caller_locations(1..2)[1]
|
28
|
-
|
31
|
+
return_defs[status] = kwargs
|
29
32
|
|
30
33
|
# response desc is required in openapi 3 – fall back to status code
|
31
34
|
return_descriptions[status] = desc || code.to_s
|
@@ -34,14 +37,19 @@ class Taro::Rails::Declaration
|
|
34
37
|
return_nestings[status] = nesting if nesting
|
35
38
|
end
|
36
39
|
|
40
|
+
# Return types are evaluated lazily to avoid unnecessary autoloading
|
41
|
+
# of all types in dev/test envs.
|
42
|
+
def returns
|
43
|
+
@returns ||= evaluate_return_defs
|
44
|
+
end
|
45
|
+
|
37
46
|
def raise_if_already_declared(status)
|
38
|
-
|
47
|
+
return_defs[status] &&
|
39
48
|
raise(Taro::ArgumentError, "response for status #{status} already declared")
|
40
49
|
end
|
41
50
|
|
42
51
|
def parse_params(rails_params)
|
43
|
-
|
44
|
-
hash
|
52
|
+
params.new(rails_params.to_unsafe_h).coerce_input
|
45
53
|
end
|
46
54
|
|
47
55
|
def finalize(controller_class:, action_name:)
|
@@ -67,10 +75,15 @@ class Taro::Rails::Declaration
|
|
67
75
|
# TODO: these change when the controller class is renamed.
|
68
76
|
# We might need a way to set `base`. Perhaps as a kwarg to `::api`?
|
69
77
|
def add_openapi_names(controller_class:, action_name:)
|
70
|
-
base = "#{controller_class.name.chomp('Controller').
|
78
|
+
base = "#{controller_class.name.chomp('Controller').gsub('::', '_')}_#{action_name}"
|
71
79
|
params.openapi_name = "#{base}_Input"
|
80
|
+
params.define_singleton_method(:name) { openapi_name }
|
81
|
+
|
72
82
|
returns.each do |status, return_type|
|
83
|
+
next if return_type.openapi_name? # only set for ad-hoc / nested return types
|
84
|
+
|
73
85
|
return_type.openapi_name = "#{base}_#{status}_Response"
|
86
|
+
return_type.define_singleton_method(:name) { openapi_name }
|
74
87
|
end
|
75
88
|
end
|
76
89
|
|
@@ -86,10 +99,34 @@ class Taro::Rails::Declaration
|
|
86
99
|
|
87
100
|
private
|
88
101
|
|
89
|
-
def
|
102
|
+
def check_return_kwargs(kwargs)
|
103
|
+
# For nested returns, evaluate_return_def calls ::field, which validates
|
104
|
+
# field options, but does not trigger type autoloading.
|
105
|
+
return evaluate_return_def(**kwargs) if kwargs[:nesting]
|
106
|
+
|
107
|
+
if kwargs.key?(:null)
|
108
|
+
raise Taro::ArgumentError, <<~MSG
|
109
|
+
`null:` is not supported for top-level returns. If you want a nullable return
|
110
|
+
value, nest it, e.g. `returns :str, type: 'String', null: true`.
|
111
|
+
MSG
|
112
|
+
end
|
113
|
+
|
114
|
+
bad_keys = kwargs.keys - (Taro::Types::Coercion.keys + %i[code desc nesting])
|
115
|
+
return if bad_keys.empty?
|
116
|
+
|
117
|
+
raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def evaluate_return_defs
|
121
|
+
return_defs.transform_values { |defi| evaluate_return_def(**defi) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def evaluate_return_def(nesting:, **kwargs)
|
90
125
|
if nesting
|
91
126
|
# ad-hoc return type, requiring the actual return type to be nested
|
92
|
-
Class.new(Taro::Types::ObjectType).tap
|
127
|
+
Class.new(Taro::Types::ObjectType).tap do |type|
|
128
|
+
type.field(nesting, null: false, **kwargs)
|
129
|
+
end
|
93
130
|
else
|
94
131
|
Taro::Types::Coercion.call(kwargs)
|
95
132
|
end
|
@@ -98,4 +135,8 @@ class Taro::Rails::Declaration
|
|
98
135
|
def raise_missing_route(controller_class, action_name)
|
99
136
|
raise(Taro::ArgumentError, "No route found for #{controller_class}##{action_name}")
|
100
137
|
end
|
138
|
+
|
139
|
+
def <=>(other)
|
140
|
+
params.openapi_name <=> other.params.openapi_name
|
141
|
+
end
|
101
142
|
end
|
@@ -12,7 +12,7 @@ class Taro::Rails::Generators::InstallGenerator < ::Rails::Generators::Base
|
|
12
12
|
def create_type_files
|
13
13
|
Dir["#{self.class.source_root}/**/*.erb"].each do |tmpl|
|
14
14
|
dest_dir = options[:dir].chomp('/')
|
15
|
-
|
15
|
+
template tmpl, "#{dest_dir}/#{File.basename(tmpl).sub('erb', 'rb')}"
|
16
16
|
end
|
17
17
|
end
|
18
18
|
# :nocov:
|
@@ -11,15 +11,20 @@ class ErrorsType < Taro::Types::ListType
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def coerce_response
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
list =
|
15
|
+
case object<%- if defined?(ActiveRecord) %>
|
16
|
+
when ActiveModel::Errors
|
17
|
+
object.errors
|
18
|
+
when ActiveRecord::Base
|
19
|
+
object.errors.errors<%- end %>
|
20
|
+
when Array
|
21
|
+
object
|
22
|
+
when Hash<%- if defined?(Interactor::Context) %>, Interactor::Context<%- end %>
|
23
|
+
object.to_h.fetch(:errors)
|
24
|
+
else
|
25
|
+
response_error("must be an Enumerable or an object with errors")
|
26
|
+
end
|
27
|
+
|
28
|
+
list.map { |el| self.class.item_type.new(el).coerce_response }
|
24
29
|
end
|
25
30
|
end
|
@@ -26,4 +26,12 @@ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
|
|
26
26
|
controller, action = rails_route.requirements.values_at(:controller, :action)
|
27
27
|
"#{controller}##{action}"
|
28
28
|
end
|
29
|
+
|
30
|
+
def can_have_request_body?
|
31
|
+
%w[patch post put].include?(verb)
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
%(#<#{self.class} "#{verb} #{openapi_path}">)
|
36
|
+
end
|
29
37
|
end
|
@@ -1,63 +1,13 @@
|
|
1
1
|
module Taro::Rails::ResponseValidation
|
2
|
-
def self.install(controller_class
|
3
|
-
|
4
|
-
|
5
|
-
key = [controller_class, action_name]
|
6
|
-
return if installed[key]
|
7
|
-
|
8
|
-
installed[key] = true
|
9
|
-
|
10
|
-
controller_class.around_action(only: action_name) do |_, block|
|
11
|
-
Taro::Types::BaseType.rendering = nil
|
12
|
-
block.call
|
13
|
-
Taro::Rails::ResponseValidation.call(self)
|
14
|
-
ensure
|
15
|
-
Taro::Types::BaseType.rendering = nil
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.installed
|
20
|
-
@installed ||= {}
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.call(controller)
|
24
|
-
declaration = Taro::Rails.declaration_for(controller)
|
25
|
-
nesting = declaration.return_nestings[controller.status]
|
26
|
-
expected = declaration.returns[controller.status]
|
27
|
-
if nesting
|
28
|
-
# case: `returns :some_nesting, type: 'SomeType'` (ad-hoc return type)
|
29
|
-
check_nesting(controller.response, nesting)
|
30
|
-
expected = expected.fields[nesting].type
|
31
|
-
end
|
32
|
-
|
33
|
-
check_expected_type_was_used(controller, expected)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.check_nesting(response, nesting)
|
37
|
-
return unless /json/.match?(response.media_type)
|
38
|
-
|
39
|
-
first_key = response.body.to_s[/\A{\s*"([^"]+)"/, 1]
|
40
|
-
first_key == nesting.to_s || raise(Taro::ResponseError, <<~MSG)
|
41
|
-
Expected response to be nested in "#{nesting}" key, but it was not.
|
42
|
-
(First JSON key in response: "#{first_key}".)
|
43
|
-
MSG
|
2
|
+
def self.install(controller_class:)
|
3
|
+
controller_class.prepend(self) if Taro.config.validate_response
|
44
4
|
end
|
45
5
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
raise(Taro::ResponseError, <<~MSG)
|
51
|
-
No matching return type declared in #{controller.class}##{controller.action_name}\
|
52
|
-
for status #{controller.status}.
|
53
|
-
MSG
|
6
|
+
def render(*, **kwargs, &)
|
7
|
+
result = super
|
8
|
+
if (declaration = Taro::Rails.declaration_for(self))
|
9
|
+
Taro::Rails::ResponseValidator.call(self, declaration, kwargs[:json])
|
54
10
|
end
|
55
|
-
|
56
|
-
used&.<=(expected) || raise(Taro::ResponseError, <<~MSG)
|
57
|
-
Expected #{controller.class}##{controller.action_name} to use #{expected}.render,
|
58
|
-
but #{used ? "#{used}.render" : 'no type render method'} was called.
|
59
|
-
MSG
|
60
|
-
|
61
|
-
Taro::Types::BaseType.used_in_response = used # for comparisons in specs
|
11
|
+
result
|
62
12
|
end
|
63
13
|
end
|