taro 2.4.0 → 3.0.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 +40 -0
- data/README.md +83 -25
- data/lib/taro/config.rb +4 -0
- data/lib/taro/declaration.rb +3 -2
- data/lib/taro/export/open_api_v3.rb +5 -4
- data/lib/taro/rails/generators/templates/error_type.erb +1 -1
- data/lib/taro/return_def.rb +1 -1
- data/lib/taro/types/base_type.rb +1 -0
- data/lib/taro/types/field.rb +51 -16
- data/lib/taro/types/field_def.rb +4 -36
- data/lib/taro/types/field_def_validation.rb +65 -0
- data/lib/taro/types/{field_validation.rb → field_value_validation.rb} +5 -1
- data/lib/taro/types/object_types/page_info_type.rb +2 -2
- data/lib/taro/types/object_types/page_type.rb +2 -2
- data/lib/taro/types/object_types/page_with_total_count_type.rb +1 -1
- data/lib/taro/types/shared/deprecation.rb +5 -1
- data/lib/taro/types/shared/object_coercion.rb +5 -2
- data/lib/taro/types/shared/openapi_format.rb +61 -0
- data/lib/taro/version.rb +1 -1
- data/tasks/benchmark.rake +5 -5
- metadata +6 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66112afc92f209f1cea91ae5483f54c37e8d5a79635ebf616afa0b868bc13637
|
|
4
|
+
data.tar.gz: fe46dcf993a314c1b2a5a08088010c523041a90bda55c121d81cf91ed665c11d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48d2ba7713e024656b49845ff1053b42076450e23367fb01063f1dce4ac3b74cd1a13351ddc43eac11e678052da76ac51508af1ba41b746e05a10cccc4c2ae4a
|
|
7
|
+
data.tar.gz: '08bd1115dcbb4240ed10edb7dbd52471c597cf5de98c85ef87be76dce9b4aef938cdc6b38ff8089c5ccfa3b684c887f75104e5ac5f9bc42eb8e3abe72e61a61c'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.0.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- there is a new `required:` property for input / param fields that defaults to `true`
|
|
8
|
+
- this is a major breaking change!
|
|
9
|
+
- `null: true` previously also allowed completely omitting a parameter
|
|
10
|
+
- now `null: true` only allows the parameter to be `null`, but it still has to be submitted
|
|
11
|
+
- set `required: false` to make parameters optional
|
|
12
|
+
- your requests will now fail by default if parameters are missing, even with `null: true`
|
|
13
|
+
- to exactly preserve the previous behavior:
|
|
14
|
+
- replace `null: true` with `null: true, required: false` in all your definitions
|
|
15
|
+
- replace `null: false` with `required: true` in all your definitions
|
|
16
|
+
- `required:` also defines which fields are marked as required in the OpenAPI export
|
|
17
|
+
- previously, all nullable fields were exported as non-required and vice versa
|
|
18
|
+
- if a `default:` is given, `required:` becomes `false` automatically
|
|
19
|
+
- missing parameters are no longer added to `@api_params` with `nil` values
|
|
20
|
+
- this is a minor breaking change
|
|
21
|
+
- previously, if a nullable param `foo` was omitted, taro added `foo: nil` to `@api_params`
|
|
22
|
+
- now, if the param is not required and not sent, it will simply be missing from `@api_params`
|
|
23
|
+
- if you relied on the previous behavior, you can set `default: nil` for the param
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- there is now a default for `null` - all fields (input & output) now default to `null: false`
|
|
28
|
+
- this is a non-breaking change because `null:` was previously a required keyword argument
|
|
29
|
+
- as a result, `null:` is now an optional argument and setting `null: false` is now redundant
|
|
30
|
+
- config options `Taro.config.default_value_for_null` and `Taro.config.default_value_for_required`
|
|
31
|
+
- option to mark whole endpoints as deprecated (`api 'My endpoint', deprecated: true`)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- improved some error messages by ensuring the field name and type are mentioned
|
|
36
|
+
|
|
37
|
+
## [2.5.0] - 2025-04-16
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Support for setting `openapi_format` for types and during export
|
|
42
|
+
|
|
3
43
|
## [2.4.0] - 2025-03-11
|
|
4
44
|
|
|
5
45
|
### Added
|
data/README.md
CHANGED
|
@@ -37,12 +37,13 @@ class BikesController < ApplicationController
|
|
|
37
37
|
api 'Update a bike', desc: 'My longer text', tags: ['Bikes']
|
|
38
38
|
|
|
39
39
|
# Params can come from the path, e.g. /bike/:id.
|
|
40
|
-
# Some types, like UUID in this case, are predefined.
|
|
41
|
-
|
|
40
|
+
# Some types, like UUID in this case, are predefined.
|
|
41
|
+
# See below for a list of all included types.
|
|
42
|
+
param :id, type: 'UUID', desc: 'ID of the bike to update'
|
|
42
43
|
|
|
43
44
|
# Params can also come from the query string or request body.
|
|
44
|
-
# This
|
|
45
|
-
param :bike, type: 'BikeInputType'
|
|
45
|
+
# This is a Hash param, implemented below.
|
|
46
|
+
param :bike, type: 'BikeInputType'
|
|
46
47
|
|
|
47
48
|
# Return types can differ by status code:
|
|
48
49
|
returns code: :ok, type: 'BikeType', desc: 'update success'
|
|
@@ -56,10 +57,11 @@ class BikesController < ApplicationController
|
|
|
56
57
|
success = bike.update(@api_params[:bike])
|
|
57
58
|
|
|
58
59
|
# Types are also used to render responses.
|
|
60
|
+
# Taro validates that the correct type is used for the declared status code.
|
|
59
61
|
if success
|
|
60
62
|
render json: BikeType.render(bike), status: :ok
|
|
61
63
|
else
|
|
62
|
-
render json: { error: MyErrorType.render(bike.errors.first) }, status: :
|
|
64
|
+
render json: { error: MyErrorType.render(bike.errors.first) }, status: :unprocessable_content
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
67
|
|
|
@@ -72,29 +74,30 @@ class BikesController < ApplicationController
|
|
|
72
74
|
end
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
Notice the multiple roles of types: They are used to describe the structure of API requests and responses,
|
|
77
|
+
Notice the multiple roles of types: They are used to describe the structure of API requests and responses, as well as to render the response. Both the input and output of the API are validated against the schema by default (see below).
|
|
76
78
|
|
|
77
|
-
Here is an example of the `BikeType`
|
|
79
|
+
Here is an example of the `BikeType` that is used to render the success response of the controller above:
|
|
78
80
|
|
|
79
81
|
```ruby
|
|
80
82
|
class BikeType < ObjectType
|
|
81
|
-
# Optional description of
|
|
83
|
+
# Optional description of the type (for the OpenAPI export)
|
|
82
84
|
self.desc = 'A bike and all relevant information about it'
|
|
83
85
|
|
|
84
|
-
# Object types have fields. Each field has a name
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
# Object types have fields. Each field has a name and its own type
|
|
87
|
+
field :wheels, type: 'Integer'
|
|
88
|
+
|
|
89
|
+
# Fields can be made nullable and can have a description
|
|
87
90
|
field :brand, type: 'String', null: true, desc: 'The brand name'
|
|
88
91
|
|
|
89
|
-
# Fields can reference other types and arrays of values
|
|
90
|
-
field :users, array_of: 'UserType'
|
|
92
|
+
# Fields can reference other custom types and arrays of values
|
|
93
|
+
field :users, array_of: 'UserType'
|
|
91
94
|
|
|
92
95
|
# Custom methods can be chosen to resolve fields
|
|
93
|
-
field :has_brand, type: 'Boolean',
|
|
96
|
+
field :has_brand, type: 'Boolean', method: :brand?
|
|
94
97
|
|
|
95
98
|
# Field resolvers can also be implemented or overridden on the type.
|
|
96
99
|
# The object passed in to `BikeType.render` is available as `object`.
|
|
97
|
-
field :fancy_info, type: 'String'
|
|
100
|
+
field :fancy_info, type: 'String'
|
|
98
101
|
def fancy_info
|
|
99
102
|
"A bike named #{object.name} with #{object.parts.count} parts."
|
|
100
103
|
end
|
|
@@ -103,14 +106,14 @@ end
|
|
|
103
106
|
|
|
104
107
|
### Input and response types
|
|
105
108
|
|
|
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.
|
|
109
|
+
You can use object types for both input and output. However, if you want added control, you can also define dedicated input and response types.
|
|
107
110
|
|
|
108
111
|
Note the use of `BikeInputType` in the `param` declaration above? It could look like so:
|
|
109
112
|
|
|
110
113
|
```ruby
|
|
111
114
|
class BikeInputType < InputType
|
|
112
|
-
field :brand, type: 'String', null: true,
|
|
113
|
-
field :wheels, type: 'Integer',
|
|
115
|
+
field :brand, type: 'String', null: true, desc: 'The brand name'
|
|
116
|
+
field :wheels, type: 'Integer', default: 2
|
|
114
117
|
end
|
|
115
118
|
```
|
|
116
119
|
|
|
@@ -121,7 +124,7 @@ Likewise, there is a special type for responses, which can't be used in input de
|
|
|
121
124
|
```ruby
|
|
122
125
|
class BikeSearchResponseType < ResponseType
|
|
123
126
|
field :bike, type: 'BikeType', null: true, desc: 'The found bike'
|
|
124
|
-
field :search_duration, type: 'Integer'
|
|
127
|
+
field :search_duration, type: 'Integer'
|
|
125
128
|
end
|
|
126
129
|
```
|
|
127
130
|
|
|
@@ -193,7 +196,7 @@ Response validation can be disabled:
|
|
|
193
196
|
Taro.config.validate_responses = false
|
|
194
197
|
```
|
|
195
198
|
|
|
196
|
-
|
|
199
|
+
#### Common error declarations
|
|
197
200
|
|
|
198
201
|
`::common_return` can be used to add a return declaration to all actions in a controller and its subclasses, and all related OpenAPI exports.
|
|
199
202
|
|
|
@@ -207,6 +210,53 @@ class AuthenticatedApiBaseController < ApiBaseController
|
|
|
207
210
|
end
|
|
208
211
|
```
|
|
209
212
|
|
|
213
|
+
#### Nullable and required fields
|
|
214
|
+
|
|
215
|
+
By default, all fields are required and non-nullable. "Required" means the field must be part of the input/output, and "non-nullable" means it can't be set to null.
|
|
216
|
+
|
|
217
|
+
To make a field nullable, use `null: true`. This goes for both input and output fields, e.g.:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
param :brand, type: 'String', null: true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This means that the `brand` param can be set to null. If you want to allow the param to be omitted entirely in requests, set `required: false`.
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
param :brand, type: 'String', null: true, required: false
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If you want to support partial updates of an attribute, but not setting it to null, you would only specify `required: false`. This will allow the param to be omitted, but submitting null for it will raise an error.
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
param :brand, type: 'String', required: false
|
|
233
|
+
def update
|
|
234
|
+
bike.update!(@api_params)
|
|
235
|
+
# ...
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The `required` option is only relevant for input fields. When rendering output, taro automatically inserts null values for missing but nullable fields, e.g.
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
BikeType.render(wheels: 2)
|
|
243
|
+
# => { "wheels": 2, "brand": null }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
It is also possible to change the default values for `null` and `required`:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
Taro.config.default_value_for_null = true
|
|
250
|
+
Taro.config.default_value_for_required = false
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Set defaults to nil to enforce explicit declaration on every param and field:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
Taro.config.default_value_for_null = nil
|
|
257
|
+
param :foo, type: 'String' # => error: null has to be specified
|
|
258
|
+
```
|
|
259
|
+
|
|
210
260
|
### Included type options
|
|
211
261
|
|
|
212
262
|
The following type names are available by default and can be used as `type:`, `array_of:`, or `page_of:` arguments:
|
|
@@ -237,7 +287,7 @@ class SeverityEnumType < EnumType
|
|
|
237
287
|
end
|
|
238
288
|
|
|
239
289
|
class ErrorType < ObjectType
|
|
240
|
-
field :severity, type: 'SeverityEnumType'
|
|
290
|
+
field :severity, type: 'SeverityEnumType'
|
|
241
291
|
end
|
|
242
292
|
```
|
|
243
293
|
|
|
@@ -245,7 +295,7 @@ Inline enums are also possible. Unlike EnumType classes, these are inlined in th
|
|
|
245
295
|
|
|
246
296
|
```ruby
|
|
247
297
|
class ErrorType < ObjectType
|
|
248
|
-
field :severity, type: 'String', enum: %w[info warning debacle]
|
|
298
|
+
field :severity, type: 'String', enum: %w[info warning debacle]
|
|
249
299
|
end
|
|
250
300
|
```
|
|
251
301
|
|
|
@@ -316,6 +366,16 @@ class BikeType < ObjectType
|
|
|
316
366
|
end
|
|
317
367
|
```
|
|
318
368
|
|
|
369
|
+
### Can I mark things as deprecated?
|
|
370
|
+
|
|
371
|
+
Yes. Endpoints, params, fields, and types can all be marked as deprecated.
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
api 'Legacy bike index, to be removed in 2043', deprecated: true
|
|
375
|
+
|
|
376
|
+
PennyFarthingType.deprecated = true
|
|
377
|
+
```
|
|
378
|
+
|
|
319
379
|
### How do I migrate from apipie-rails?
|
|
320
380
|
|
|
321
381
|
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.
|
|
@@ -330,7 +390,6 @@ If you want to migrate anyway:
|
|
|
330
390
|
|
|
331
391
|
- extract complex param declarations into InputTypes
|
|
332
392
|
- extract complex response declarations into ObjectTypes or ResponseTypes
|
|
333
|
-
- replace `required: true` with `null: false` and `required: false` with `null: true`
|
|
334
393
|
|
|
335
394
|
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`:
|
|
336
395
|
|
|
@@ -410,8 +469,7 @@ end
|
|
|
410
469
|
- mixed arrays
|
|
411
470
|
- mixed enums
|
|
412
471
|
- nullable enums
|
|
413
|
-
- string
|
|
414
|
-
- string minLength and maxLength (substitute: `self.pattern = /\A.{3,5}\z/`)
|
|
472
|
+
- string minLength and maxLength (substitute: `MyType.pattern = /\A.{3,5}\z/`)
|
|
415
473
|
- number minimum, exclusiveMinimum, maximum, multipleOf
|
|
416
474
|
- readOnly, writeOnly
|
|
417
475
|
|
data/lib/taro/config.rb
CHANGED
|
@@ -2,6 +2,8 @@ module Taro::Config
|
|
|
2
2
|
singleton_class.attr_accessor(
|
|
3
3
|
:api_name,
|
|
4
4
|
:api_version,
|
|
5
|
+
:default_value_for_null,
|
|
6
|
+
:default_value_for_required,
|
|
5
7
|
:export_format,
|
|
6
8
|
:export_path,
|
|
7
9
|
:parse_params,
|
|
@@ -12,6 +14,8 @@ module Taro::Config
|
|
|
12
14
|
# defaults
|
|
13
15
|
self.api_name = 'Taro-based API'
|
|
14
16
|
self.api_version = '1.0'
|
|
17
|
+
self.default_value_for_null = false
|
|
18
|
+
self.default_value_for_required = true
|
|
15
19
|
self.export_format = :yaml
|
|
16
20
|
self.export_path = 'api.yml'
|
|
17
21
|
self.parse_params = true
|
data/lib/taro/declaration.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Descendants must implement #endpoint and (only for openapi export) #routes.
|
|
3
3
|
# See Taro::Rails::Declaration for an example.
|
|
4
4
|
class Taro::Declaration
|
|
5
|
-
attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :tags
|
|
5
|
+
attr_reader :deprecated, :desc, :summary, :params, :return_defs, :return_descriptions, :tags
|
|
6
6
|
|
|
7
7
|
def initialize(for_klass = nil)
|
|
8
8
|
@params = Class.new(Taro::Types::RailsParamsType)
|
|
@@ -12,9 +12,10 @@ class Taro::Declaration
|
|
|
12
12
|
Taro::CommonReturns.for(for_klass).each { |rd| add_return_def(rd) }
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def add_info(summary, desc: nil, tags: nil)
|
|
15
|
+
def add_info(summary, deprecated: nil, desc: nil, tags: nil)
|
|
16
16
|
summary.is_a?(String) || raise(Taro::ArgumentError, 'api summary must be a String')
|
|
17
17
|
@summary = summary
|
|
18
|
+
@deprecated = true if deprecated
|
|
18
19
|
@desc = desc
|
|
19
20
|
@tags = Array(tags) if tags
|
|
20
21
|
end
|
|
@@ -26,6 +26,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
|
26
26
|
def export_route(route, declaration)
|
|
27
27
|
{
|
|
28
28
|
route.verb.to_sym => {
|
|
29
|
+
deprecated: declaration.deprecated,
|
|
29
30
|
description: declaration.desc,
|
|
30
31
|
summary: declaration.summary,
|
|
31
32
|
tags: declaration.tags,
|
|
@@ -70,7 +71,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
|
70
71
|
name: field.name,
|
|
71
72
|
deprecated: field.deprecated,
|
|
72
73
|
description: field.desc,
|
|
73
|
-
required:
|
|
74
|
+
required: field.required,
|
|
74
75
|
schema: export_field(field).except(:deprecated, :description),
|
|
75
76
|
}.compact
|
|
76
77
|
end
|
|
@@ -129,7 +130,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
|
129
130
|
|
|
130
131
|
def export_type(type)
|
|
131
132
|
if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
|
|
132
|
-
{ type: type.openapi_type }
|
|
133
|
+
{ type: type.openapi_type, format: type.openapi_format }.compact
|
|
133
134
|
else
|
|
134
135
|
extract_component_ref(type)
|
|
135
136
|
end
|
|
@@ -148,7 +149,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
def export_scalar_field(field)
|
|
151
|
-
base = { type: field.openapi_type }
|
|
152
|
+
base = { type: field.openapi_type, format: field.openapi_format }.compact
|
|
152
153
|
# Using oneOf seems more correct than an array of types
|
|
153
154
|
# as it puts props like format together with the main type.
|
|
154
155
|
# https://github.com/OAI/OpenAPI-Specification/issues/3148
|
|
@@ -199,7 +200,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
|
199
200
|
end
|
|
200
201
|
|
|
201
202
|
def object_type_details(type)
|
|
202
|
-
required = type.fields.values.
|
|
203
|
+
required = type.fields.values.select(&:required).map(&:name)
|
|
203
204
|
{
|
|
204
205
|
type: type.openapi_type,
|
|
205
206
|
deprecated: type.deprecated,
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
# You can customize these files to fit your needs, or delete them.
|
|
6
6
|
class ErrorType < ObjectType
|
|
7
7
|
field :attribute, type: 'String', null: true, desc: 'Attribute name'
|
|
8
|
-
field :code, type: 'String',
|
|
8
|
+
field :code, type: 'String', method: :type, desc: 'Error code'
|
|
9
9
|
field :message, type: 'String', null: true, desc: 'Error message'
|
|
10
10
|
end
|
data/lib/taro/return_def.rb
CHANGED
|
@@ -14,7 +14,7 @@ class Taro::ReturnDef
|
|
|
14
14
|
def evaluate
|
|
15
15
|
if nesting
|
|
16
16
|
Class.new(Taro::Types::NestedResponseType).tap do |type|
|
|
17
|
-
type.field(nesting, defined_at:,
|
|
17
|
+
type.field(nesting, defined_at:, **params)
|
|
18
18
|
end
|
|
19
19
|
else
|
|
20
20
|
Taro::Types::Coercion.call(params)
|
data/lib/taro/types/base_type.rb
CHANGED
|
@@ -17,6 +17,7 @@ Taro::Types::BaseType = Struct.new(:object) do
|
|
|
17
17
|
extend Taro::Types::Shared::Description
|
|
18
18
|
extend Taro::Types::Shared::Equivalence
|
|
19
19
|
extend Taro::Types::Shared::Name
|
|
20
|
+
extend Taro::Types::Shared::OpenAPIFormat
|
|
20
21
|
extend Taro::Types::Shared::OpenAPIName
|
|
21
22
|
extend Taro::Types::Shared::OpenAPIType
|
|
22
23
|
extend Taro::Types::Shared::Rendering
|
data/lib/taro/types/field.rb
CHANGED
|
@@ -1,25 +1,58 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative 'field_value_validation'
|
|
2
2
|
|
|
3
|
-
Taro::Types::Field = Data.define(
|
|
4
|
-
|
|
3
|
+
Taro::Types::Field = Data.define(
|
|
4
|
+
:name, :type,
|
|
5
|
+
:default, :defined_at, :deprecated, :desc, :enum, :null, :required, :resolver,
|
|
6
|
+
) do
|
|
7
|
+
include Taro::Types::FieldValueValidation
|
|
5
8
|
include Taro::Types::Shared::Errors
|
|
6
9
|
include Taro::Types::Shared::TypeClass
|
|
7
10
|
|
|
8
|
-
def initialize(
|
|
11
|
+
def initialize(
|
|
12
|
+
name:,
|
|
13
|
+
type:,
|
|
14
|
+
default: Taro::None,
|
|
15
|
+
defined_at: nil,
|
|
16
|
+
deprecated: nil,
|
|
17
|
+
desc: nil,
|
|
18
|
+
enum: nil,
|
|
19
|
+
method: name, # note: `method` is stored as #resolver to avoid overriding Object#method
|
|
20
|
+
null: Taro.config.default_value_for_null,
|
|
21
|
+
required: default == Taro::None ? Taro.config.default_value_for_required : false
|
|
22
|
+
)
|
|
9
23
|
enum = coerce_to_enum(enum)
|
|
10
|
-
super(
|
|
24
|
+
super(
|
|
25
|
+
name:,
|
|
26
|
+
type:,
|
|
27
|
+
default:,
|
|
28
|
+
defined_at:,
|
|
29
|
+
deprecated: (true if deprecated),
|
|
30
|
+
desc:,
|
|
31
|
+
enum:,
|
|
32
|
+
null: !!null,
|
|
33
|
+
required: !!required,
|
|
34
|
+
resolver: method,
|
|
35
|
+
)
|
|
11
36
|
end
|
|
12
37
|
|
|
13
38
|
def value_for_input(object)
|
|
14
|
-
|
|
39
|
+
unless object&.key?(name)
|
|
40
|
+
fail_if_required
|
|
41
|
+
return default_specified? ? default : Taro::None
|
|
42
|
+
end
|
|
43
|
+
value = object[name]
|
|
15
44
|
value = coerce_value(value, true)
|
|
16
45
|
validated_value(value)
|
|
46
|
+
rescue Taro::ValidationError => e
|
|
47
|
+
reraise_recursively_with_path_info(e)
|
|
17
48
|
end
|
|
18
49
|
|
|
19
50
|
def value_for_response(object, context: nil, object_is_hash: true)
|
|
20
51
|
value = retrieve_response_value(object, context, object_is_hash)
|
|
21
52
|
value = coerce_value(value, false)
|
|
22
53
|
validated_value(value, false)
|
|
54
|
+
rescue Taro::ValidationError => e
|
|
55
|
+
reraise_recursively_with_path_info(e)
|
|
23
56
|
end
|
|
24
57
|
|
|
25
58
|
def default_specified?
|
|
@@ -30,6 +63,10 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
|
30
63
|
type.openapi_type
|
|
31
64
|
end
|
|
32
65
|
|
|
66
|
+
def openapi_format
|
|
67
|
+
type.openapi_format
|
|
68
|
+
end
|
|
69
|
+
|
|
33
70
|
private
|
|
34
71
|
|
|
35
72
|
def coerce_to_enum(arg)
|
|
@@ -42,22 +79,22 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
|
42
79
|
end
|
|
43
80
|
|
|
44
81
|
def retrieve_response_value(object, context, object_is_hash)
|
|
45
|
-
if context&.resolve?(
|
|
46
|
-
context.public_send(
|
|
82
|
+
if context&.resolve?(resolver)
|
|
83
|
+
context.public_send(resolver)
|
|
47
84
|
elsif object_is_hash
|
|
48
85
|
retrieve_hash_value(object)
|
|
49
|
-
elsif object.respond_to?(
|
|
50
|
-
object.public_send(
|
|
86
|
+
elsif object.respond_to?(resolver, true)
|
|
87
|
+
object.public_send(resolver)
|
|
51
88
|
else
|
|
52
|
-
response_error "No such method or resolver `:#{
|
|
89
|
+
response_error "No such method or resolver `:#{resolver}`", object
|
|
53
90
|
end
|
|
54
91
|
end
|
|
55
92
|
|
|
56
93
|
def retrieve_hash_value(object)
|
|
57
|
-
if object.key?(
|
|
58
|
-
object[
|
|
94
|
+
if object.key?(resolver.to_s)
|
|
95
|
+
object[resolver.to_s]
|
|
59
96
|
else
|
|
60
|
-
object[
|
|
97
|
+
object[resolver]
|
|
61
98
|
end
|
|
62
99
|
end
|
|
63
100
|
|
|
@@ -67,8 +104,6 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
|
67
104
|
|
|
68
105
|
type_obj = type.new(value)
|
|
69
106
|
from_input ? type_obj.coerce_input : type_obj.cached_coerce_response
|
|
70
|
-
rescue Taro::ValidationError => e
|
|
71
|
-
reraise_recursively_with_path_info(e)
|
|
72
107
|
end
|
|
73
108
|
|
|
74
109
|
def reraise_recursively_with_path_info(error)
|
data/lib/taro/types/field_def.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
require_relative 'field_def_validation'
|
|
2
|
+
|
|
1
3
|
# Lazily-evaluated field definition.
|
|
2
4
|
class Taro::Types::FieldDef
|
|
5
|
+
include Taro::Types::FieldDefValidation
|
|
6
|
+
|
|
3
7
|
attr_reader :attributes, :defined_at
|
|
4
8
|
|
|
5
9
|
def initialize(defined_at: nil, **attributes)
|
|
@@ -23,40 +27,4 @@ class Taro::Types::FieldDef
|
|
|
23
27
|
def ==(other)
|
|
24
28
|
other.is_a?(self.class) && attributes == other.attributes
|
|
25
29
|
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def validate
|
|
30
|
-
validate_name
|
|
31
|
-
validate_null
|
|
32
|
-
validate_type_key
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def validate_name
|
|
36
|
-
name.is_a?(Symbol) || raise(Taro::ArgumentError, <<~MSG)
|
|
37
|
-
field name must be a Symbol, got #{name.class} at #{defined_at}
|
|
38
|
-
MSG
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def validate_null
|
|
42
|
-
[true, false].include?(attributes[:null]) || raise(Taro::ArgumentError, <<~MSG)
|
|
43
|
-
null has to be specified as true or false for field #{name} at #{defined_at}"
|
|
44
|
-
MSG
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def validate_type_key
|
|
48
|
-
attributes[type_key].class == String || raise(Taro::ArgumentError, <<~MSG)
|
|
49
|
-
#{type_key} must be a String for field #{name} at #{defined_at}
|
|
50
|
-
MSG
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def type_key
|
|
54
|
-
possible_keys = Taro::Types::Coercion.keys
|
|
55
|
-
keys = attributes.keys & possible_keys
|
|
56
|
-
keys.size == 1 || raise(Taro::ArgumentError, <<~MSG)
|
|
57
|
-
Exactly one of #{possible_keys.join(', ')} must be given
|
|
58
|
-
for field #{name} at #{defined_at}
|
|
59
|
-
MSG
|
|
60
|
-
keys.first
|
|
61
|
-
end
|
|
62
30
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Taro::Types::FieldDefValidation
|
|
2
|
+
private
|
|
3
|
+
|
|
4
|
+
def validate
|
|
5
|
+
validate_name
|
|
6
|
+
validate_null
|
|
7
|
+
validate_required
|
|
8
|
+
validate_required_default_conflict
|
|
9
|
+
validate_type_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def validate_name
|
|
13
|
+
name.is_a?(Symbol) || raise(Taro::ArgumentError, <<~MSG)
|
|
14
|
+
field name must be a Symbol, got #{name.class} at #{defined_at}
|
|
15
|
+
MSG
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_null
|
|
19
|
+
if attributes.key?(:null)
|
|
20
|
+
[true, false].include?(attributes[:null]) || raise(Taro::ArgumentError, <<~MSG)
|
|
21
|
+
null has to be specified as true or false for field #{name} at #{defined_at}"
|
|
22
|
+
MSG
|
|
23
|
+
elsif Taro.config.default_value_for_null.nil?
|
|
24
|
+
raise(Taro::ArgumentError, <<~MSG)
|
|
25
|
+
null has to be specified for field #{name} at #{defined_at} because there is no default
|
|
26
|
+
MSG
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_required
|
|
31
|
+
if attributes.key?(:required)
|
|
32
|
+
[true, false].include?(attributes[:required]) || raise(Taro::ArgumentError, <<~MSG)
|
|
33
|
+
required has to be specified as true or false for field #{name} at #{defined_at}
|
|
34
|
+
MSG
|
|
35
|
+
elsif Taro.config.default_value_for_required.nil?
|
|
36
|
+
raise(Taro::ArgumentError, <<~MSG)
|
|
37
|
+
required has to be specified for field #{name} at #{defined_at} because there is no default
|
|
38
|
+
MSG
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_required_default_conflict
|
|
43
|
+
return unless attributes[:required] && attributes.key?(:default)
|
|
44
|
+
|
|
45
|
+
raise(Taro::ArgumentError, <<~MSG)
|
|
46
|
+
required: true cannot be combined with a default for field #{name} at #{defined_at}
|
|
47
|
+
MSG
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_type_key
|
|
51
|
+
attributes[type_key].class == String || raise(Taro::ArgumentError, <<~MSG)
|
|
52
|
+
#{type_key} must be a String for field #{name} at #{defined_at}
|
|
53
|
+
MSG
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def type_key
|
|
57
|
+
possible_keys = Taro::Types::Coercion.keys
|
|
58
|
+
keys = attributes.keys & possible_keys
|
|
59
|
+
keys.size == 1 || raise(Taro::ArgumentError, <<~MSG)
|
|
60
|
+
Exactly one of #{possible_keys.join(', ')} must be given
|
|
61
|
+
for field #{name} at #{defined_at}
|
|
62
|
+
MSG
|
|
63
|
+
keys.first
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module Taro::Types::
|
|
1
|
+
module Taro::Types::FieldValueValidation
|
|
2
2
|
# Validate the value against the field properties. This method will raise
|
|
3
3
|
# a Taro::InputError or Taro::ResponseError if the value is not matching.
|
|
4
4
|
def validated_value(value, for_input = true)
|
|
@@ -22,4 +22,8 @@ module Taro::Types::FieldValidation
|
|
|
22
22
|
msg = "field expects one of #{enum.inspect}, got #{value.inspect}"
|
|
23
23
|
for_input ? input_error(msg, value) : response_error(msg, value)
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
def fail_if_required
|
|
27
|
+
required && input_error('field is required but key is missing', nil)
|
|
28
|
+
end
|
|
25
29
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class Taro::Types::ObjectTypes::PageInfoType < Taro::Types::ResponseType
|
|
2
2
|
self.openapi_name = 'PageInfo'
|
|
3
3
|
|
|
4
|
-
field :has_previous_page, type: 'Boolean',
|
|
5
|
-
field :has_next_page, type: 'Boolean',
|
|
4
|
+
field :has_previous_page, type: 'Boolean', desc: 'Whether there is a previous page of results'
|
|
5
|
+
field :has_next_page, type: 'Boolean', desc: 'Whether there is another page of results'
|
|
6
6
|
field :start_cursor, type: 'String', null: true, desc: 'The first cursor in the current page of results (null if zero results)'
|
|
7
7
|
field :end_cursor, type: 'String', null: true, desc: 'The last cursor in the current page of results (null if zero results)'
|
|
8
8
|
end
|
|
@@ -9,8 +9,8 @@ class Taro::Types::ObjectTypes::PageType < Taro::Types::ResponseType
|
|
|
9
9
|
|
|
10
10
|
def self.derive_from(from_type)
|
|
11
11
|
super
|
|
12
|
-
field(:page, array_of: from_type.name
|
|
13
|
-
field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType'
|
|
12
|
+
field(:page, array_of: from_type.name)
|
|
13
|
+
field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType')
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.render(relation, **)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
class Taro::Types::ObjectTypes::PageWithTotalCountType < Taro::Types::ObjectTypes::PageType
|
|
12
12
|
def self.derive_from(from_type)
|
|
13
13
|
super
|
|
14
|
-
field(:total_count, type: 'Integer'
|
|
14
|
+
field(:total_count, type: 'Integer')
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.paginate(relation, **)
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
module Taro::Types::Shared::ObjectCoercion
|
|
3
3
|
def coerce_input
|
|
4
4
|
validate_no_undeclared_params
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
result = {}
|
|
6
|
+
self.class.fields.each do |name, field|
|
|
7
|
+
value = field.value_for_input(object)
|
|
8
|
+
result[name] = value unless value.equal?(Taro::None)
|
|
7
9
|
end
|
|
10
|
+
result
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
# Render the object into a hash.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Provides a setter and getter for type classes' `openapi_format`,
|
|
2
|
+
# for use in the OpenAPI export.
|
|
3
|
+
module Taro::Types::Shared::OpenAPIFormat
|
|
4
|
+
OPENAPI_STRING_FORMATS = %i[
|
|
5
|
+
date
|
|
6
|
+
date-time
|
|
7
|
+
password
|
|
8
|
+
byte
|
|
9
|
+
binary
|
|
10
|
+
email
|
|
11
|
+
uuid
|
|
12
|
+
uri
|
|
13
|
+
hostname
|
|
14
|
+
ipv4
|
|
15
|
+
ipv6
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
OPENAPI_INTEGER_FORMATS = %i[
|
|
19
|
+
int32
|
|
20
|
+
int64
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
OPENAPI_NUMBER_FORMATS = %i[
|
|
24
|
+
float
|
|
25
|
+
double
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def openapi_format
|
|
29
|
+
return unless @openapi_format
|
|
30
|
+
|
|
31
|
+
unless valid_formats_for_openapi_type.include?(@openapi_format)
|
|
32
|
+
raise(Taro::ArgumentError, "openapi_format #{@openapi_format.inspect} is invalid for openapi_type #{@openapi_type.inspect}, must be one for #{valid_formats_for_openapi_type}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@openapi_format
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def openapi_format=(arg)
|
|
39
|
+
@openapi_format = arg
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inherited(subclass)
|
|
43
|
+
subclass.instance_variable_set(:@openapi_format, @openapi_format)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def valid_formats_for_openapi_type
|
|
50
|
+
case @openapi_type
|
|
51
|
+
when :string
|
|
52
|
+
OPENAPI_STRING_FORMATS
|
|
53
|
+
when :integer
|
|
54
|
+
OPENAPI_INTEGER_FORMATS
|
|
55
|
+
when :number
|
|
56
|
+
OPENAPI_NUMBER_FORMATS
|
|
57
|
+
else
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/taro/version.rb
CHANGED
data/tasks/benchmark.rake
CHANGED
|
@@ -6,11 +6,11 @@ task :benchmark do
|
|
|
6
6
|
data = JSON.load_file("#{__dir__}/benchmark_1kb.json", symbolize_names: true)
|
|
7
7
|
|
|
8
8
|
item_type = Class.new(Taro::Types::ObjectType) do
|
|
9
|
-
field :name, type: 'String'
|
|
10
|
-
field :language, type: 'String'
|
|
11
|
-
field :id, type: 'String'
|
|
12
|
-
field :bio, type: 'String'
|
|
13
|
-
field :version, type: 'Float'
|
|
9
|
+
field :name, type: 'String'
|
|
10
|
+
field :language, type: 'String'
|
|
11
|
+
field :id, type: 'String'
|
|
12
|
+
field :bio, type: 'String'
|
|
13
|
+
field :version, type: 'Float'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
type = item_type.array
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: taro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janosch Müller
|
|
8
8
|
- Johannes Opper
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: exe
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: rack
|
|
@@ -27,7 +26,6 @@ dependencies:
|
|
|
27
26
|
version: '3.0'
|
|
28
27
|
description: This library provides an object-based type system for RESTful Ruby APIs,
|
|
29
28
|
with built-in parameter parsing, response rendering, and OpenAPI schema export.
|
|
30
|
-
email:
|
|
31
29
|
executables: []
|
|
32
30
|
extensions: []
|
|
33
31
|
extra_rdoc_files: []
|
|
@@ -80,7 +78,8 @@ files:
|
|
|
80
78
|
- lib/taro/types/enum_type.rb
|
|
81
79
|
- lib/taro/types/field.rb
|
|
82
80
|
- lib/taro/types/field_def.rb
|
|
83
|
-
- lib/taro/types/
|
|
81
|
+
- lib/taro/types/field_def_validation.rb
|
|
82
|
+
- lib/taro/types/field_value_validation.rb
|
|
84
83
|
- lib/taro/types/input_type.rb
|
|
85
84
|
- lib/taro/types/list_type.rb
|
|
86
85
|
- lib/taro/types/nested_response_type.rb
|
|
@@ -115,6 +114,7 @@ files:
|
|
|
115
114
|
- lib/taro/types/shared/item_type.rb
|
|
116
115
|
- lib/taro/types/shared/name.rb
|
|
117
116
|
- lib/taro/types/shared/object_coercion.rb
|
|
117
|
+
- lib/taro/types/shared/openapi_format.rb
|
|
118
118
|
- lib/taro/types/shared/openapi_name.rb
|
|
119
119
|
- lib/taro/types/shared/openapi_type.rb
|
|
120
120
|
- lib/taro/types/shared/pattern.rb
|
|
@@ -131,7 +131,6 @@ metadata:
|
|
|
131
131
|
homepage_uri: https://github.com/taro-rb/taro
|
|
132
132
|
source_code_uri: https://github.com/taro-rb/taro
|
|
133
133
|
changelog_uri: https://github.com/taro-rb/taro/CHANGELOG.md
|
|
134
|
-
post_install_message:
|
|
135
134
|
rdoc_options: []
|
|
136
135
|
require_paths:
|
|
137
136
|
- lib
|
|
@@ -146,8 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
146
145
|
- !ruby/object:Gem::Version
|
|
147
146
|
version: '0'
|
|
148
147
|
requirements: []
|
|
149
|
-
rubygems_version:
|
|
150
|
-
signing_key:
|
|
148
|
+
rubygems_version: 4.0.3
|
|
151
149
|
specification_version: 4
|
|
152
150
|
summary: Typed Api using Ruby Objects.
|
|
153
151
|
test_files: []
|