taro 1.1.0 → 1.3.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 +39 -0
- data/README.md +76 -9
- data/lib/taro/errors.rb +1 -1
- data/lib/taro/export/open_api_v3.rb +76 -24
- data/lib/taro/rails/declaration.rb +44 -20
- 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/tasks/export.rake +5 -1
- data/lib/taro/types/base_type.rb +2 -0
- data/lib/taro/types/coercion.rb +24 -14
- 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 +3 -3
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +3 -3
- data/lib/taro/types/scalar/string_type.rb +17 -6
- data/lib/taro/types/scalar/timestamp_type.rb +1 -0
- data/lib/taro/types/scalar/uuid_v4_type.rb +3 -20
- data/lib/taro/types/scalar_type.rb +1 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
- 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/pattern.rb +69 -0
- data/lib/taro/types/shared/rendering.rb +4 -4
- 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: 41f14035882a39e03a7bae9c799a42c9fa63414e4201fbbab7f876d54d8d214b
|
4
|
+
data.tar.gz: 768b505506f8ad58792d0d86b6152f1aa60da98419aef111b9fc187ee74eab42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 27607c95ed9c5a7ab2c4d579dec41357657f24c694f43eac06b15677a82e491fd5986c5cad8b7bf999ee23ee456d75cc2c90debee47e93d02cdc901b4e45f498
|
7
|
+
data.tar.gz: f99bfdfe66f7a09ac1746c23e2671bcec7f20cfee23e980db76561f14e1ea082bc2fcc59e4650af8732d7e608a8b7c012473dc0f862bb15ff9d89501a641c825
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,47 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.3.0] - 2024-11-25
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Support for string patterns (on StringType children and in export)
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
- Fixed OpenAPI export of params with enum (inline & type-based)
|
12
|
+
|
13
|
+
## [1.2.0] - 2024-11-18
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- Improved error messages
|
18
|
+
- Option to define custom derived types
|
19
|
+
- Option to use custom keys in paginated content
|
20
|
+
- Option to deprecate individual fields, params, and types
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
|
24
|
+
- Fixed nullable enum fields raising for null input
|
25
|
+
- Fixed auto-loading of return types
|
26
|
+
- Fixed console spam when inspecting declarations
|
27
|
+
- Fixed resolver method not being used when rendering a Hash
|
28
|
+
- Fixed the ErrorsType template
|
29
|
+
- Many fixes for OpenAPI export
|
30
|
+
- Fixed export of parameters for http methods without body
|
31
|
+
- Fixed export for PageType
|
32
|
+
- Fixed export for arrays of UUIDs, Dates, and Times
|
33
|
+
- Fixed export YML keys for namespaced controllers
|
34
|
+
- Reference plain types for repeated flat return types
|
35
|
+
- Made order of paths, verbs, responses and schemas deterministic
|
36
|
+
|
3
37
|
## [1.1.0] - 2024-11-16
|
4
38
|
|
39
|
+
### Added
|
40
|
+
|
5
41
|
- Response validation refined
|
42
|
+
|
43
|
+
### Fixed
|
44
|
+
|
6
45
|
- Bugfix for openapi export
|
7
46
|
|
8
47
|
## [1.0.0] - 2024-11-14
|
data/README.md
CHANGED
@@ -131,16 +131,18 @@ Taro.config.validate_responses = false
|
|
131
131
|
The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
|
132
132
|
|
133
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'`
|
134
136
|
- `'Float'`
|
135
137
|
- `'FreeForm'` - accepts and renders any JSON-serializable object, use with care
|
136
138
|
- `'Integer'`
|
137
139
|
- `'NoContent'` - renders an empty object, for use with `status: :no_content`
|
138
140
|
- `'String'`
|
141
|
+
- `'Time'` - accepts and renders a time string in ISO8601 format
|
139
142
|
- `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
|
140
143
|
- `'UUID'` - accepts and renders UUIDs
|
141
|
-
|
142
|
-
|
143
|
-
- `'DateTime'` - an alias for `'Time'`
|
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.
|
144
146
|
|
145
147
|
### Enums
|
146
148
|
|
@@ -168,7 +170,7 @@ end
|
|
168
170
|
|
169
171
|
### FAQ
|
170
172
|
|
171
|
-
#### How
|
173
|
+
#### How do I avoid repeating common error declarations?
|
172
174
|
|
173
175
|
Hook into the DSL in your base controller(s):
|
174
176
|
|
@@ -197,7 +199,7 @@ class AuthenticatedApiController < ApiBaseController
|
|
197
199
|
end
|
198
200
|
```
|
199
201
|
|
200
|
-
#### How
|
202
|
+
#### How do I use context in my types?
|
201
203
|
|
202
204
|
Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
|
203
205
|
|
@@ -211,13 +213,80 @@ class BikeType < ObjectType
|
|
211
213
|
end
|
212
214
|
```
|
213
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
|
+
|
214
255
|
#### Why do I have to use type name strings instead of the type constants?
|
215
256
|
|
216
257
|
Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
|
217
258
|
|
218
259
|
The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
|
219
260
|
|
220
|
-
|
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
|
+
```
|
221
290
|
|
222
291
|
## Possible future features
|
223
292
|
|
@@ -227,7 +296,6 @@ This already works fo type classes – they don't trigger loading of referenced
|
|
227
296
|
- sum types
|
228
297
|
- api doc rendering based on export (e.g. rails engine with web ui)
|
229
298
|
- [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
|
230
|
-
- deprecation feature
|
231
299
|
- maybe make `type:` optional for path params as they're always strings anyway
|
232
300
|
- various openapi features
|
233
301
|
- non-JSON content types (e.g. for file uploads)
|
@@ -237,8 +305,7 @@ This already works fo type classes – they don't trigger loading of referenced
|
|
237
305
|
- mixed enums
|
238
306
|
- nullable enums
|
239
307
|
- string format specifications (e.g. binary, int64, password ...)
|
240
|
-
- string pattern
|
241
|
-
- string minLength and maxLength
|
308
|
+
- string minLength and maxLength (substitute: `self.pattern = /\A.{3,5}\z/`)
|
242
309
|
- number minimum, exclusiveMinimum, maximum, multipleOf
|
243
310
|
- readOnly, writeOnly
|
244
311
|
|
data/lib/taro/errors.rb
CHANGED
@@ -11,13 +11,13 @@ 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
22
|
paths[route.openapi_path] ||= {}
|
23
23
|
paths[route.openapi_path].merge! export_route(route, declaration)
|
@@ -31,30 +31,58 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
31
31
|
description: declaration.desc,
|
32
32
|
summary: declaration.summary,
|
33
33
|
tags: declaration.tags,
|
34
|
-
parameters:
|
34
|
+
parameters: route_parameters(declaration, route),
|
35
35
|
requestBody: request_body(declaration, route),
|
36
36
|
responses: responses(declaration),
|
37
37
|
}.compact,
|
38
38
|
}
|
39
39
|
end
|
40
40
|
|
41
|
+
def route_parameters(declaration, route)
|
42
|
+
path_parameters(declaration, route) + query_parameters(declaration, route)
|
43
|
+
end
|
44
|
+
|
41
45
|
def path_parameters(declaration, route)
|
42
46
|
route.path_params.map do |param_name|
|
43
47
|
param_field = declaration.params.fields[param_name] || raise(<<~MSG)
|
44
48
|
Declaration missing for path param #{param_name} of route #{route.endpoint}
|
45
49
|
MSG
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
# path params are always required in rails
|
52
|
+
export_parameter(param_field).merge(in: 'path', required: true)
|
53
|
+
end
|
54
|
+
end
|
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')
|
54
63
|
end
|
55
64
|
end
|
56
65
|
|
66
|
+
def export_parameter(field)
|
67
|
+
validate_path_or_query_parameter(field)
|
68
|
+
|
69
|
+
{
|
70
|
+
name: field.name,
|
71
|
+
deprecated: field.deprecated,
|
72
|
+
description: field.desc,
|
73
|
+
required: !field.null,
|
74
|
+
schema: export_field(field).except(:deprecated, :description),
|
75
|
+
}.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_path_or_query_parameter(field)
|
79
|
+
[:array, :object].include?(field.type.openapi_type) &&
|
80
|
+
raise("Unexpected object as path/query param #{field.name}: #{field.type}")
|
81
|
+
end
|
82
|
+
|
57
83
|
def request_body(declaration, route)
|
84
|
+
return unless route.can_have_request_body?
|
85
|
+
|
58
86
|
params = declaration.params
|
59
87
|
body_param_fields = params.fields.reject do |name, _field|
|
60
88
|
route.path_params.include?(name)
|
@@ -66,7 +94,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
66
94
|
body_input_type.openapi_name = params.openapi_name
|
67
95
|
|
68
96
|
# For polymorphic routes (more than one for the same declaration),
|
69
|
-
# we can't use refs because
|
97
|
+
# we can't use refs because their request body might differ:
|
70
98
|
# Different params might be in the path vs. in the request body.
|
71
99
|
use_refs = !declaration.polymorphic_route?
|
72
100
|
schema = request_body_schema(body_input_type, use_refs:)
|
@@ -82,7 +110,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
82
110
|
end
|
83
111
|
|
84
112
|
def responses(declaration)
|
85
|
-
declaration.returns.to_h do |code, type|
|
113
|
+
declaration.returns.sort.to_h do |code, type|
|
86
114
|
[
|
87
115
|
code.to_s,
|
88
116
|
{
|
@@ -94,13 +122,17 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
94
122
|
end
|
95
123
|
|
96
124
|
def export_type(type)
|
97
|
-
if type < Taro::Types::ScalarType
|
125
|
+
if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
|
98
126
|
{ type: type.openapi_type }
|
99
127
|
else
|
100
128
|
extract_component_ref(type)
|
101
129
|
end
|
102
130
|
end
|
103
131
|
|
132
|
+
def custom_scalar_type?(type)
|
133
|
+
type < Taro::Types::ScalarType && (type.openapi_pattern || type.deprecated)
|
134
|
+
end
|
135
|
+
|
104
136
|
def export_field(field)
|
105
137
|
if field.type < Taro::Types::ScalarType
|
106
138
|
export_scalar_field(field)
|
@@ -115,23 +147,29 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
115
147
|
# as it puts props like format together with the main type.
|
116
148
|
# https://github.com/OAI/OpenAPI-Specification/issues/3148
|
117
149
|
base = { oneOf: [base, { type: 'null' }] } if field.null
|
118
|
-
base
|
119
|
-
base[:default] = field.default if field.default_specified?
|
120
|
-
base[:enum] = field.enum if field.enum
|
121
|
-
base
|
150
|
+
base.merge(field_metadata(field))
|
122
151
|
end
|
123
152
|
|
124
153
|
def export_complex_field_ref(field)
|
125
154
|
ref = extract_component_ref(field.type)
|
155
|
+
return ref if field_metadata(field).empty? && !field.null
|
156
|
+
|
126
157
|
if field.null
|
127
158
|
# RE nullable: https://stackoverflow.com/a/70658334
|
128
|
-
{
|
129
|
-
|
159
|
+
{ oneOf: [ref, { type: 'null' }] }
|
160
|
+
else # i.e. with metadata such as description or deprecated
|
130
161
|
# https://github.com/OAI/OpenAPI-Specification/issues/2033
|
131
|
-
{
|
132
|
-
|
133
|
-
|
134
|
-
|
162
|
+
{ allOf: [ref] }
|
163
|
+
end.merge(field_metadata(field))
|
164
|
+
end
|
165
|
+
|
166
|
+
def field_metadata(field)
|
167
|
+
meta = {}
|
168
|
+
meta[:description] = field.desc if field.desc
|
169
|
+
meta[:deprecated] = field.deprecated unless field.deprecated.nil?
|
170
|
+
meta[:default] = field.default if field.default_specified?
|
171
|
+
meta[:enum] = field.enum if field.enum
|
172
|
+
meta
|
135
173
|
end
|
136
174
|
|
137
175
|
def extract_component_ref(type)
|
@@ -147,6 +185,8 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
147
185
|
enum_type_details(type)
|
148
186
|
elsif type < Taro::Types::ListType
|
149
187
|
list_type_details(type)
|
188
|
+
elsif custom_scalar_type?(type)
|
189
|
+
custom_scalar_type_details(type)
|
150
190
|
else
|
151
191
|
raise NotImplementedError, "Unsupported type: #{type}"
|
152
192
|
end
|
@@ -156,6 +196,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
156
196
|
required = type.fields.values.reject(&:null).map(&:name)
|
157
197
|
{
|
158
198
|
type: type.openapi_type,
|
199
|
+
deprecated: type.deprecated,
|
159
200
|
description: type.desc,
|
160
201
|
required: (required if required.any?),
|
161
202
|
properties: type.fields.to_h { |name, f| [name, export_field(f)] },
|
@@ -166,6 +207,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
166
207
|
def enum_type_details(enum)
|
167
208
|
{
|
168
209
|
type: enum.item_type.openapi_type,
|
210
|
+
deprecated: enum.deprecated,
|
169
211
|
description: enum.desc,
|
170
212
|
enum: enum.values,
|
171
213
|
}.compact
|
@@ -174,11 +216,21 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
174
216
|
def list_type_details(list)
|
175
217
|
{
|
176
218
|
type: 'array',
|
219
|
+
deprecated: list.deprecated,
|
177
220
|
description: list.desc,
|
178
221
|
items: export_type(list.item_type),
|
179
222
|
}.compact
|
180
223
|
end
|
181
224
|
|
225
|
+
def custom_scalar_type_details(scalar)
|
226
|
+
{
|
227
|
+
type: scalar.openapi_type,
|
228
|
+
deprecated: scalar.deprecated,
|
229
|
+
description: scalar.desc,
|
230
|
+
pattern: scalar.openapi_pattern,
|
231
|
+
}.compact
|
232
|
+
end
|
233
|
+
|
182
234
|
def assert_unique_openapi_name(type)
|
183
235
|
@name_to_type_map ||= {}
|
184
236
|
if (prev = @name_to_type_map[type.openapi_name]) && type != prev
|
@@ -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,19 +99,11 @@ class Taro::Rails::Declaration
|
|
86
99
|
|
87
100
|
private
|
88
101
|
|
89
|
-
def return_type_from(nesting, **kwargs)
|
90
|
-
if nesting
|
91
|
-
# ad-hoc return type, requiring the actual return type to be nested
|
92
|
-
Class.new(Taro::Types::ObjectType).tap do |type|
|
93
|
-
type.field(nesting, null: false, **kwargs)
|
94
|
-
end
|
95
|
-
else
|
96
|
-
check_return_kwargs(kwargs)
|
97
|
-
Taro::Types::Coercion.call(kwargs)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
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
|
+
|
102
107
|
if kwargs.key?(:null)
|
103
108
|
raise Taro::ArgumentError, <<~MSG
|
104
109
|
`null:` is not supported for top-level returns. If you want a nullable return
|
@@ -106,13 +111,32 @@ class Taro::Rails::Declaration
|
|
106
111
|
MSG
|
107
112
|
end
|
108
113
|
|
109
|
-
bad_keys = kwargs.keys - (Taro::Types::Coercion
|
114
|
+
bad_keys = kwargs.keys - (Taro::Types::Coercion.keys + %i[code desc nesting])
|
110
115
|
return if bad_keys.empty?
|
111
116
|
|
112
117
|
raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
|
113
118
|
end
|
114
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)
|
125
|
+
if nesting
|
126
|
+
# ad-hoc return type, requiring the actual return type to be nested
|
127
|
+
Class.new(Taro::Types::ObjectType).tap do |type|
|
128
|
+
type.field(nesting, null: false, **kwargs)
|
129
|
+
end
|
130
|
+
else
|
131
|
+
Taro::Types::Coercion.call(kwargs)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
115
135
|
def raise_missing_route(controller_class, action_name)
|
116
136
|
raise(Taro::ArgumentError, "No route found for #{controller_class}##{action_name}")
|
117
137
|
end
|
138
|
+
|
139
|
+
def <=>(other)
|
140
|
+
params.openapi_name <=> other.params.openapi_name
|
141
|
+
end
|
118
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
|
@@ -10,6 +10,10 @@ task 'taro:export' => :environment do
|
|
10
10
|
version: Taro.config.api_version,
|
11
11
|
)
|
12
12
|
|
13
|
-
data = export.
|
13
|
+
data = export.send("to_#{Taro.config.export_format}")
|
14
|
+
|
15
|
+
FileUtils.mkdir_p(File.dirname(Taro.config.export_path))
|
14
16
|
File.write(Taro.config.export_path, data)
|
17
|
+
|
18
|
+
puts "Exported #{Taro.config.api_name} to #{Taro.config.export_path}"
|
15
19
|
end
|
data/lib/taro/types/base_type.rb
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
Taro::Types::BaseType = Data.define(:object) do
|
10
10
|
require_relative "shared"
|
11
11
|
extend Taro::Types::Shared::AdditionalProperties
|
12
|
+
extend Taro::Types::Shared::Deprecation
|
13
|
+
extend Taro::Types::Shared::DerivedTypes
|
12
14
|
extend Taro::Types::Shared::Description
|
13
15
|
extend Taro::Types::Shared::OpenAPIName
|
14
16
|
extend Taro::Types::Shared::OpenAPIType
|
data/lib/taro/types/coercion.rb
CHANGED
@@ -1,42 +1,52 @@
|
|
1
1
|
module Taro::Types::Coercion
|
2
|
-
KEYS = %i[type array_of page_of].freeze
|
3
|
-
|
4
2
|
class << self
|
5
3
|
def call(arg)
|
6
4
|
validate_hash(arg)
|
7
5
|
from_hash(arg)
|
8
6
|
end
|
9
7
|
|
8
|
+
# Coercion keys can be expanded by the DerivedTypes module.
|
9
|
+
def keys
|
10
|
+
@keys ||= %i[type]
|
11
|
+
end
|
12
|
+
|
13
|
+
def derived_suffix
|
14
|
+
'_of'
|
15
|
+
end
|
16
|
+
|
10
17
|
private
|
11
18
|
|
12
19
|
def validate_hash(arg)
|
13
20
|
arg.is_a?(Hash) || raise(Taro::ArgumentError, <<~MSG)
|
14
|
-
Type coercion argument must be a Hash, got: #{arg.
|
21
|
+
Type coercion argument must be a Hash, got: #{arg.class}
|
15
22
|
MSG
|
16
23
|
|
17
|
-
types = arg.slice(*
|
24
|
+
types = arg.slice(*keys)
|
18
25
|
types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
|
19
|
-
Exactly one of
|
26
|
+
Exactly one of #{keys.join(', ')} must be given, got: #{types}
|
20
27
|
MSG
|
21
28
|
end
|
22
29
|
|
23
30
|
def from_hash(hash)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
31
|
+
keys.each do |key|
|
32
|
+
next unless (value = hash[key])
|
33
|
+
|
34
|
+
# e.g. `returns type: 'MyType'` -> MyType
|
35
|
+
return from_string(value) if key == :type
|
36
|
+
|
37
|
+
# DerivedTypes
|
38
|
+
# e.g. `returns array_of: 'MyType'` -> MyType.array
|
39
|
+
return from_string(value).send(key.to_s.chomp(derived_suffix))
|
32
40
|
end
|
41
|
+
|
42
|
+
raise NotImplementedError, "Unsupported type coercion #{hash}"
|
33
43
|
end
|
34
44
|
|
35
45
|
def from_string(arg)
|
36
46
|
shortcuts[arg] || from_class(Object.const_get(arg.to_s))
|
37
47
|
rescue NameError
|
38
48
|
raise Taro::ArgumentError, <<~MSG
|
39
|
-
|
49
|
+
No such type: #{arg}. It should be a type-class name
|
40
50
|
or one of #{shortcuts.keys.map(&:inspect).join(', ')}.
|
41
51
|
MSG
|
42
52
|
end
|