taro 2.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8f629d8573ed34c2d60303a387e99cae4cc9fcb0b14901d7080def1e27d0d90
4
- data.tar.gz: 40c3ae606ade65922c5b69f9dd86353f2740d061b6d7094f61abe906e5f9198f
3
+ metadata.gz: 66112afc92f209f1cea91ae5483f54c37e8d5a79635ebf616afa0b868bc13637
4
+ data.tar.gz: fe46dcf993a314c1b2a5a08088010c523041a90bda55c121d81cf91ed665c11d
5
5
  SHA512:
6
- metadata.gz: 34b8e8d9ef5c2f016f56a887b674fa243ae6074fc59e4d29a1cb1873c4dfc3e87d8c24a37dd93a461ad71fc0fd38849462360bed7f6f9a1cca89e37fb3ad192b
7
- data.tar.gz: d99f3bdc2e40e555a1a0b54dcac6a62d5774b6ae2c7c0d5e05c5ee0f1457864af696b37949173d8189769b03b5d9ce589eb1f49d855209c5c2bccadf32b51d84
6
+ metadata.gz: 48d2ba7713e024656b49845ff1053b42076450e23367fb01063f1dce4ac3b74cd1a13351ddc43eac11e678052da76ac51508af1ba41b746e05a10cccc4c2ae4a
7
+ data.tar.gz: '08bd1115dcbb4240ed10edb7dbd52471c597cf5de98c85ef87be76dce9b4aef938cdc6b38ff8089c5ccfa3b684c887f75104e5ac5f9bc42eb8e3abe72e61a61c'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
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
+
3
37
  ## [2.5.0] - 2025-04-16
4
38
 
5
39
  ### 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. See below for more types.
41
- param :id, type: 'UUID', null: false, desc: 'ID of the bike to update'
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 describes a Hash param:
45
- param :bike, type: 'BikeInputType', null: false
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: :unprocessable_entity
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, and render the response. Both the input and output of the API are validated against the schema by default (see below).
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` from the controller above:
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 BikeType (for the OpenAPI export)
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, its own type,
85
- # and a `null:` setting to indicate if it can be nil.
86
- # Providing a description is optional.
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', null: false
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', null: false, method: :brand?
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', null: false
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, desc: 'The brand name'
113
- field :wheels, type: 'Integer', null: false, default: 2
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', null: false
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
- ### Common error declarations
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', null: false
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], null: false
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,7 +469,7 @@ end
410
469
  - mixed arrays
411
470
  - mixed enums
412
471
  - nullable enums
413
- - string minLength and maxLength (substitute: `self.pattern = /\A.{3,5}\z/`)
472
+ - string minLength and maxLength (substitute: `MyType.pattern = /\A.{3,5}\z/`)
414
473
  - number minimum, exclusiveMinimum, maximum, multipleOf
415
474
  - readOnly, writeOnly
416
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
@@ -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: !field.null,
74
+ required: field.required,
74
75
  schema: export_field(field).except(:deprecated, :description),
75
76
  }.compact
76
77
  end
@@ -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.reject(&:null).map(&:name)
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', null: false, method: :type, desc: 'Error code'
8
+ field :code, type: 'String', method: :type, desc: 'Error code'
9
9
  field :message, type: 'String', null: true, desc: 'Error message'
10
10
  end
@@ -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:, null: false, **params)
17
+ type.field(nesting, defined_at:, **params)
18
18
  end
19
19
  else
20
20
  Taro::Types::Coercion.call(params)
@@ -1,25 +1,58 @@
1
- require_relative 'field_validation'
1
+ require_relative 'field_value_validation'
2
2
 
3
- Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc, :deprecated) do
4
- include Taro::Types::FieldValidation
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(name:, type:, null:, method: name, default: Taro::None, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
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(name:, type:, null:, method:, default:, enum:, defined_at:, desc:, deprecated:)
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
- value = object[name] if object
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?
@@ -46,22 +79,22 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
46
79
  end
47
80
 
48
81
  def retrieve_response_value(object, context, object_is_hash)
49
- if context&.resolve?(method)
50
- context.public_send(method)
82
+ if context&.resolve?(resolver)
83
+ context.public_send(resolver)
51
84
  elsif object_is_hash
52
85
  retrieve_hash_value(object)
53
- elsif object.respond_to?(method, true)
54
- object.public_send(method)
86
+ elsif object.respond_to?(resolver, true)
87
+ object.public_send(resolver)
55
88
  else
56
- response_error "No such method or resolver `:#{method}`", object
89
+ response_error "No such method or resolver `:#{resolver}`", object
57
90
  end
58
91
  end
59
92
 
60
93
  def retrieve_hash_value(object)
61
- if object.key?(method.to_s)
62
- object[method.to_s]
94
+ if object.key?(resolver.to_s)
95
+ object[resolver.to_s]
63
96
  else
64
- object[method]
97
+ object[resolver]
65
98
  end
66
99
  end
67
100
 
@@ -71,8 +104,6 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
71
104
 
72
105
  type_obj = type.new(value)
73
106
  from_input ? type_obj.coerce_input : type_obj.cached_coerce_response
74
- rescue Taro::ValidationError => e
75
- reraise_recursively_with_path_info(e)
76
107
  end
77
108
 
78
109
  def reraise_recursively_with_path_info(error)
@@ -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::FieldValidation
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', null: false, desc: 'Whether there is a previous page of results'
5
- field :has_next_page, type: 'Boolean', null: false, desc: 'Whether there is another page of results'
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, null: false)
13
- field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType', null: false)
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', null: false)
14
+ field(:total_count, type: 'Integer')
15
15
  end
16
16
 
17
17
  def self.paginate(relation, **)
@@ -1,3 +1,7 @@
1
1
  module Taro::Types::Shared::Deprecation
2
- attr_accessor :deprecated
2
+ attr_reader :deprecated
3
+
4
+ def deprecated=(value)
5
+ @deprecated = value ? true : nil
6
+ end
3
7
  end
@@ -2,9 +2,12 @@
2
2
  module Taro::Types::Shared::ObjectCoercion
3
3
  def coerce_input
4
4
  validate_no_undeclared_params
5
- self.class.fields.transform_values do |field|
6
- field.value_for_input(object)
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.
data/lib/taro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # :nocov:
2
2
  module Taro
3
- VERSION = "2.5.0"
3
+ VERSION = "3.0.0"
4
4
  end
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', null: false
10
- field :language, type: 'String', null: false
11
- field :id, type: 'String', null: false
12
- field :bio, type: 'String', null: false
13
- field :version, type: 'Float', null: false
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: 2.5.0
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: 2025-04-16 00:00:00.000000000 Z
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/field_validation.rb
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
@@ -132,7 +131,6 @@ metadata:
132
131
  homepage_uri: https://github.com/taro-rb/taro
133
132
  source_code_uri: https://github.com/taro-rb/taro
134
133
  changelog_uri: https://github.com/taro-rb/taro/CHANGELOG.md
135
- post_install_message:
136
134
  rdoc_options: []
137
135
  require_paths:
138
136
  - lib
@@ -147,8 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
145
  - !ruby/object:Gem::Version
148
146
  version: '0'
149
147
  requirements: []
150
- rubygems_version: 3.5.16
151
- signing_key:
148
+ rubygems_version: 4.0.3
152
149
  specification_version: 4
153
150
  summary: Typed Api using Ruby Objects.
154
151
  test_files: []