taro 1.4.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +138 -60
- data/lib/taro/cache.rb +14 -0
- data/lib/taro/common_returns.rb +31 -0
- data/lib/taro/declaration.rb +82 -0
- data/lib/taro/declarations.rb +34 -0
- data/lib/taro/errors.rb +15 -2
- data/lib/taro/export/base.rb +1 -1
- data/lib/taro/export/open_api_v3.rb +20 -23
- data/lib/taro/export.rb +1 -1
- data/lib/taro/none.rb +2 -0
- data/lib/taro/rails/active_declarations.rb +2 -10
- data/lib/taro/rails/declaration.rb +9 -114
- data/lib/taro/rails/declaration_buffer.rb +2 -1
- data/lib/taro/rails/dsl.rb +13 -6
- data/lib/taro/rails/generators/install_generator.rb +1 -1
- data/lib/taro/rails/generators/templates/errors_type.erb +1 -1
- data/lib/taro/rails/generators/templates/response_type.erb +4 -0
- data/lib/taro/rails/generators.rb +1 -1
- data/lib/taro/rails/normalized_route.rb +20 -38
- data/lib/taro/rails/param_parsing.rb +5 -3
- data/lib/taro/rails/railtie.rb +4 -0
- data/lib/taro/rails/response_validator.rb +53 -52
- data/lib/taro/rails/route_finder.rb +5 -7
- data/lib/taro/rails/tasks/export.rake +10 -9
- data/lib/taro/rails.rb +2 -3
- data/lib/taro/return_def.rb +43 -0
- data/lib/taro/route.rb +32 -0
- data/lib/taro/status_code.rb +16 -0
- data/lib/taro/types/base_type.rb +7 -1
- data/lib/taro/types/coercion.rb +2 -2
- data/lib/taro/types/enum_type.rb +1 -1
- data/lib/taro/types/field.rb +17 -5
- data/lib/taro/types/field_def.rb +62 -0
- data/lib/taro/types/field_validation.rb +4 -6
- data/lib/taro/types/input_type.rb +4 -9
- data/lib/taro/types/list_type.rb +1 -1
- data/lib/taro/types/nested_response_type.rb +16 -0
- data/lib/taro/types/object_type.rb +2 -7
- data/lib/taro/types/object_types/no_content_type.rb +1 -5
- data/lib/taro/types/object_types/page_info_type.rb +1 -1
- data/lib/taro/types/object_types/page_type.rb +1 -5
- data/lib/taro/types/response_type.rb +8 -0
- data/lib/taro/types/scalar/integer_param_type.rb +15 -0
- data/lib/taro/types/scalar_type.rb +1 -1
- data/lib/taro/types/shared/caching.rb +30 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
- data/lib/taro/types/shared/derived_types.rb +34 -15
- data/lib/taro/types/shared/equivalence.rb +14 -0
- data/lib/taro/types/shared/errors.rb +8 -8
- data/lib/taro/types/shared/fields.rb +10 -36
- data/lib/taro/types/shared/name.rb +14 -0
- data/lib/taro/types/shared/object_coercion.rb +0 -13
- data/lib/taro/types/shared/openapi_name.rb +0 -6
- data/lib/taro/types/shared/rendering.rb +5 -3
- data/lib/taro/types/shared.rb +1 -1
- data/lib/taro/types.rb +1 -1
- data/lib/taro/version.rb +1 -1
- data/lib/taro.rb +6 -1
- metadata +19 -3
data/lib/taro/types/base_type.rb
CHANGED
@@ -6,14 +6,20 @@
|
|
6
6
|
# Instances of types are initialized with the object that they represent.
|
7
7
|
# The object is a parameter hash for inputs and a manually passed hash
|
8
8
|
# or object when rendering a response.
|
9
|
-
|
9
|
+
#
|
10
|
+
# Using Struct instead of Data here for performance reasons:
|
11
|
+
# https://bugs.ruby-lang.org/issues/19693
|
12
|
+
Taro::Types::BaseType = Struct.new(:object) do
|
10
13
|
require_relative "shared"
|
11
14
|
extend Taro::Types::Shared::AdditionalProperties
|
12
15
|
extend Taro::Types::Shared::Deprecation
|
13
16
|
extend Taro::Types::Shared::DerivedTypes
|
14
17
|
extend Taro::Types::Shared::Description
|
18
|
+
extend Taro::Types::Shared::Equivalence
|
19
|
+
extend Taro::Types::Shared::Name
|
15
20
|
extend Taro::Types::Shared::OpenAPIName
|
16
21
|
extend Taro::Types::Shared::OpenAPIType
|
17
22
|
extend Taro::Types::Shared::Rendering
|
23
|
+
include Taro::Types::Shared::Caching
|
18
24
|
include Taro::Types::Shared::Errors
|
19
25
|
end
|
data/lib/taro/types/coercion.rb
CHANGED
@@ -64,7 +64,7 @@ module Taro::Types::Coercion
|
|
64
64
|
require 'date'
|
65
65
|
def shortcuts
|
66
66
|
@shortcuts ||= {
|
67
|
-
# rubocop:disable Layout/HashAlignment
|
67
|
+
# rubocop:disable Layout/HashAlignment
|
68
68
|
'Boolean' => Taro::Types::Scalar::BooleanType,
|
69
69
|
'Date' => Taro::Types::Scalar::ISO8601DateType,
|
70
70
|
'DateTime' => Taro::Types::Scalar::ISO8601DateTimeType,
|
@@ -76,7 +76,7 @@ module Taro::Types::Coercion
|
|
76
76
|
'Time' => Taro::Types::Scalar::ISO8601DateTimeType,
|
77
77
|
'Timestamp' => Taro::Types::Scalar::TimestampType,
|
78
78
|
'UUID' => Taro::Types::Scalar::UUIDv4Type,
|
79
|
-
# rubocop:enable Layout/HashAlignment
|
79
|
+
# rubocop:enable Layout/HashAlignment
|
80
80
|
}.freeze
|
81
81
|
end
|
82
82
|
end
|
data/lib/taro/types/enum_type.rb
CHANGED
@@ -24,7 +24,7 @@ class Taro::Types::EnumType < Taro::Types::BaseType
|
|
24
24
|
|
25
25
|
def coerce_response
|
26
26
|
self.class.raise_if_empty_enum
|
27
|
-
value = self.class.item_type.new(object).
|
27
|
+
value = self.class.item_type.new(object).cached_coerce_response
|
28
28
|
if self.class.values.include?(value)
|
29
29
|
value
|
30
30
|
else
|
data/lib/taro/types/field.rb
CHANGED
@@ -2,8 +2,9 @@ require_relative 'field_validation'
|
|
2
2
|
|
3
3
|
Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc, :deprecated) do
|
4
4
|
include Taro::Types::FieldValidation
|
5
|
+
include Taro::Types::Shared::Errors
|
5
6
|
|
6
|
-
def initialize(name:, type:, null:, method: name, default:
|
7
|
+
def initialize(name:, type:, null:, method: name, default: Taro::None, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
|
7
8
|
enum = coerce_to_enum(enum)
|
8
9
|
super(name:, type:, null:, method:, default:, enum:, defined_at:, desc:, deprecated:)
|
9
10
|
end
|
@@ -21,7 +22,7 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def default_specified?
|
24
|
-
!default.equal?(
|
25
|
+
!default.equal?(Taro::None)
|
25
26
|
end
|
26
27
|
|
27
28
|
def openapi_type
|
@@ -47,8 +48,7 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
47
48
|
elsif object.respond_to?(method, true)
|
48
49
|
object.public_send(method)
|
49
50
|
else
|
50
|
-
|
51
|
-
raise Taro::ResponseError, "No such method or resolver `:#{method}`."
|
51
|
+
response_error "No such method or resolver `:#{method}`", object
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -65,6 +65,18 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
65
65
|
return default if value.nil? && default_specified?
|
66
66
|
|
67
67
|
type_obj = type.new(value)
|
68
|
-
from_input ? type_obj.coerce_input : type_obj.
|
68
|
+
from_input ? type_obj.coerce_input : type_obj.cached_coerce_response
|
69
|
+
rescue Taro::ValidationError => e
|
70
|
+
reraise_recursively_with_path_info(e)
|
71
|
+
end
|
72
|
+
|
73
|
+
def reraise_recursively_with_path_info(error)
|
74
|
+
msg =
|
75
|
+
error
|
76
|
+
.message
|
77
|
+
.sub(/ at `\K/, "#{name}.")
|
78
|
+
.sub(/(is not valid as [^`]+)(?=: )/, "\\1 at `#{name}`")
|
79
|
+
|
80
|
+
raise error.class.new(msg, error.object, error.origin)
|
69
81
|
end
|
70
82
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Lazily-evaluated field definition.
|
2
|
+
class Taro::Types::FieldDef
|
3
|
+
attr_reader :attributes, :defined_at
|
4
|
+
|
5
|
+
def initialize(defined_at: nil, **attributes)
|
6
|
+
@attributes = attributes
|
7
|
+
@defined_at = defined_at
|
8
|
+
validate
|
9
|
+
end
|
10
|
+
|
11
|
+
def evaluate
|
12
|
+
Taro::Types::Field.new(
|
13
|
+
**attributes.except(*Taro::Types::Coercion.keys),
|
14
|
+
defined_at:,
|
15
|
+
type: Taro::Types::Coercion.call(attributes),
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
attributes[:name]
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.is_a?(self.class) && attributes == other.attributes
|
25
|
+
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
|
+
end
|
@@ -12,16 +12,14 @@ module Taro::Types::FieldValidation
|
|
12
12
|
def validate_null_and_ok?(value, for_input)
|
13
13
|
return if null || !value.nil?
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
MSG
|
15
|
+
msg = 'field is not nullable'
|
16
|
+
for_input ? input_error(msg, value) : response_error(msg, value)
|
18
17
|
end
|
19
18
|
|
20
19
|
def validate_enum_inclusion(value, for_input)
|
21
20
|
return if enum.nil? || null && value.nil? || enum.include?(value)
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
MSG
|
22
|
+
msg = "field expects one of #{enum.inspect}, got #{value.inspect}"
|
23
|
+
for_input ? input_error(msg, value) : response_error(msg, value)
|
26
24
|
end
|
27
25
|
end
|
@@ -1,13 +1,8 @@
|
|
1
|
-
|
2
|
-
class Taro::Types::InputType < Taro::Types::BaseType
|
3
|
-
require_relative "shared"
|
4
|
-
extend Taro::Types::Shared::Fields
|
5
|
-
include Taro::Types::Shared::CustomFieldResolvers
|
6
|
-
include Taro::Types::Shared::ObjectCoercion
|
7
|
-
|
8
|
-
self.openapi_type = :object
|
1
|
+
require_relative 'object_type'
|
9
2
|
|
3
|
+
# Abstract base class for input types, i.e. types without response rendering.
|
4
|
+
class Taro::Types::InputType < Taro::Types::ObjectType
|
10
5
|
def coerce_response
|
11
|
-
response_error
|
6
|
+
response_error "#{self.class.name} is an InputType and cannot be used as response type"
|
12
7
|
end
|
13
8
|
end
|
data/lib/taro/types/list_type.rb
CHANGED
@@ -17,7 +17,7 @@ class Taro::Types::ListType < Taro::Types::BaseType
|
|
17
17
|
object.respond_to?(:map) || response_error('must be an Enumerable')
|
18
18
|
|
19
19
|
item_type = self.class.item_type
|
20
|
-
object.map { |el| item_type.new(el).
|
20
|
+
object.map { |el| item_type.new(el).cached_coerce_response }
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.default_openapi_name
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'response_type'
|
2
|
+
|
3
|
+
# @api private - this type is only for internal use in Declarations.
|
4
|
+
class Taro::Types::NestedResponseType < Taro::Types::ResponseType
|
5
|
+
def self.nesting_field
|
6
|
+
fields.size == 1 || raise(
|
7
|
+
Taro::InvariantError, "#{self} should have 1 field, got #{fields}"
|
8
|
+
)
|
9
|
+
fields.each_value.first
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.default_openapi_name
|
13
|
+
field = nesting_field
|
14
|
+
"#{field.type.openapi_name}_in_#{field.name}_Response"
|
15
|
+
end
|
16
|
+
end
|
@@ -6,14 +6,9 @@ class Taro::Types::ObjectType < Taro::Types::BaseType
|
|
6
6
|
include Taro::Types::Shared::ObjectCoercion
|
7
7
|
|
8
8
|
self.openapi_type = :object
|
9
|
-
|
10
|
-
def self.inherited(subclass)
|
11
|
-
subclass.instance_variable_set(:@response_types, [Hash])
|
12
|
-
subclass.instance_variable_set(:@input_types, [Hash])
|
13
|
-
super
|
14
|
-
end
|
15
9
|
end
|
16
10
|
|
17
11
|
module Taro::Types::ObjectTypes
|
18
|
-
|
12
|
+
require_relative 'response_type'
|
13
|
+
Dir[File.join(__dir__, 'object_types', '**', '*.rb')].each { |f| require_relative f }
|
19
14
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Taro::Types::ObjectTypes::NoContentType < Taro::Types::
|
1
|
+
class Taro::Types::ObjectTypes::NoContentType < Taro::Types::ResponseType
|
2
2
|
self.desc = 'An empty response'
|
3
3
|
self.openapi_name = 'NoContent'
|
4
4
|
|
@@ -7,10 +7,6 @@ class Taro::Types::ObjectTypes::NoContentType < Taro::Types::ObjectType
|
|
7
7
|
super(nil)
|
8
8
|
end
|
9
9
|
|
10
|
-
def coerce_input
|
11
|
-
input_error 'NoContentType cannot be used as input type'
|
12
|
-
end
|
13
|
-
|
14
10
|
def coerce_response
|
15
11
|
{}
|
16
12
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Taro::Types::ObjectTypes::PageInfoType < Taro::Types::
|
1
|
+
class Taro::Types::ObjectTypes::PageInfoType < Taro::Types::ResponseType
|
2
2
|
self.openapi_name = 'PageInfo'
|
3
3
|
|
4
4
|
field :has_previous_page, type: 'Boolean', null: false, desc: 'Whether there is a previous page of results'
|
@@ -4,7 +4,7 @@
|
|
4
4
|
#
|
5
5
|
# The gem rails_cursor_pagination must be installed to use this.
|
6
6
|
#
|
7
|
-
class Taro::Types::ObjectTypes::PageType < Taro::Types::
|
7
|
+
class Taro::Types::ObjectTypes::PageType < Taro::Types::ResponseType
|
8
8
|
extend Taro::Types::Shared::ItemType
|
9
9
|
|
10
10
|
def self.derive_from(from_type)
|
@@ -13,10 +13,6 @@ class Taro::Types::ObjectTypes::PageType < Taro::Types::ObjectType
|
|
13
13
|
field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType', null: false)
|
14
14
|
end
|
15
15
|
|
16
|
-
def coerce_input
|
17
|
-
input_error 'PageTypes cannot be used as input types'
|
18
|
-
end
|
19
|
-
|
20
16
|
def self.render(relation, after:, limit: 20, order_by: nil, order: nil)
|
21
17
|
result = RailsCursorPagination::Paginator.new(
|
22
18
|
relation, limit:, order_by:, order:, after:
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require_relative 'object_type'
|
2
|
+
|
3
|
+
# Abstract base class for response types, i.e. types without input parsing.
|
4
|
+
class Taro::Types::ResponseType < Taro::Types::ObjectType
|
5
|
+
def coerce_input
|
6
|
+
input_error "#{self.class.name} is a ResponseType and cannot be used as input type"
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# @api private – relaxed Integer type for use with path & query params,
|
2
|
+
# which Rails provides as Strings in ActionController::Parameters.
|
3
|
+
class Taro::Types::Scalar::IntegerParamType < Taro::Types::ScalarType
|
4
|
+
self.openapi_type = :integer
|
5
|
+
|
6
|
+
def coerce_input
|
7
|
+
if object.is_a?(Integer)
|
8
|
+
object
|
9
|
+
elsif object.is_a?(String) && object.match?(/\A\d+\z/)
|
10
|
+
object.to_i
|
11
|
+
else
|
12
|
+
input_error('must be an Integer')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Taro::Types::Shared::Caching
|
2
|
+
def cached_coerce_response
|
3
|
+
Taro::Cache.call(object, cache_key: self.class.cache_key, expires_in: self.class.expires_in) do
|
4
|
+
coerce_response
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend(ClassMethods)
|
10
|
+
klass.singleton_class.attr_accessor :expires_in, :without_cache
|
11
|
+
klass.singleton_class.attr_reader :cache_key
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def cache_key=(arg)
|
16
|
+
arg.nil? || arg.is_a?(Proc) && arg.arity == 1 || arg.is_a?(Hash) ||
|
17
|
+
raise(Taro::ArgumentError, "Type.cache_key must be a Proc with arity 1, a Hash, or nil")
|
18
|
+
|
19
|
+
@cache_key = arg
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_cache(cache_key:, expires_in: nil)
|
23
|
+
klass = dup
|
24
|
+
klass.cache_key = cache_key.is_a?(Proc) ? cache_key : ->(_) { cache_key }
|
25
|
+
klass.expires_in = expires_in
|
26
|
+
klass.without_cache = self
|
27
|
+
klass
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -15,9 +15,9 @@ module Taro::Types::Shared::CustomFieldResolvers
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def method_added(name)
|
18
|
-
if [
|
18
|
+
if %i[input_error object pattern response_error].include?(name)
|
19
19
|
raise(Taro::ArgumentError, "##{name} is a reserved, internally used method name")
|
20
|
-
elsif
|
20
|
+
elsif !%i[coerce_input coerce_response].include?(name) &&
|
21
21
|
!self.name.to_s.start_with?('Taro::Types::')
|
22
22
|
custom_resolvers[name] = true
|
23
23
|
end
|
@@ -1,27 +1,46 @@
|
|
1
1
|
module Taro::Types::Shared::DerivedTypes
|
2
2
|
# Adds `name` as a method to all type classes and adds
|
3
|
-
#
|
3
|
+
# :`name`_of as a supported key to the Coercion module.
|
4
4
|
# When `name` is called on a type class T, it returns a new subclass
|
5
5
|
# S inheriting from `type` and passes T to S::derive_from.
|
6
|
-
def define_derived_type(name,
|
7
|
-
|
8
|
-
|
6
|
+
def define_derived_type(name, derivable_type)
|
7
|
+
add_coercion_key(name)
|
8
|
+
add_derivation_method(name, derivable_type)
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def derived_types
|
12
|
+
Taro::Types::Shared::DerivedTypes.map[self] ||= {}
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def self.map
|
16
|
+
@map ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def add_coercion_key(base_name)
|
22
|
+
new_key = :"#{base_name}#{Taro::Types::Coercion.derived_suffix}"
|
23
|
+
if Taro::Types::Coercion.keys.include?(new_key)
|
24
|
+
raise ArgumentError, "#{new_key} is already in use"
|
19
25
|
end
|
20
26
|
|
21
|
-
|
27
|
+
Taro::Types::Coercion.keys << new_key
|
22
28
|
end
|
23
29
|
|
24
|
-
def
|
25
|
-
|
30
|
+
def add_derivation_method(method_name, type)
|
31
|
+
root = Taro::Types::BaseType
|
32
|
+
if root.respond_to?(method_name)
|
33
|
+
raise ArgumentError, "#{method_name} is already in use"
|
34
|
+
end
|
35
|
+
|
36
|
+
root.define_singleton_method(method_name) do
|
37
|
+
derived_types[type] ||= begin
|
38
|
+
type_class = Taro::Types::Coercion.call(type:)
|
39
|
+
new_type = Class.new(type_class)
|
40
|
+
new_type.define_name("#{self.name}.#{method_name}")
|
41
|
+
new_type.derive_from(self)
|
42
|
+
new_type
|
43
|
+
end
|
44
|
+
end
|
26
45
|
end
|
27
46
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Taro::Types::Shared::Equivalence
|
2
|
+
def equivalent?(other)
|
3
|
+
equal?(other) || equal_properties?(other)
|
4
|
+
end
|
5
|
+
|
6
|
+
def equal_properties?(other)
|
7
|
+
return false unless other.openapi_type == openapi_type
|
8
|
+
|
9
|
+
# @fields is lazy-loaded. Comparing @field_defs suffices.
|
10
|
+
ignored = %i[@fields]
|
11
|
+
(instance_variables - ignored).to_h { |i| [i, instance_variable_get(i)] } ==
|
12
|
+
(other.instance_variables - ignored).to_h { |i| [i, other.instance_variable_get(i)] }
|
13
|
+
end
|
14
|
+
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
module Taro::Types::Shared::Errors
|
2
|
-
def input_error(msg)
|
3
|
-
raise Taro::InputError
|
2
|
+
def input_error(msg, value = object)
|
3
|
+
raise Taro::InputError.new(coerce_error_message(msg, value), value, self)
|
4
4
|
end
|
5
5
|
|
6
|
-
def response_error(msg)
|
7
|
-
raise Taro::ResponseError
|
6
|
+
def response_error(msg, value = object)
|
7
|
+
raise Taro::ResponseError.new(coerce_error_message(msg, value), value, self)
|
8
8
|
end
|
9
9
|
|
10
|
-
def coerce_error_message(msg)
|
11
|
-
|
12
|
-
|
13
|
-
"#{
|
10
|
+
def coerce_error_message(msg, value)
|
11
|
+
type_class = is_a?(Taro::Types::Field) ? self.type : self.class
|
12
|
+
type_desc = type_class.name.sub(/^Taro::Types::.*?([^:]+)$/, '\1')
|
13
|
+
"#{value.class} is not valid as #{type_desc}: #{msg}"
|
14
14
|
end
|
15
15
|
end
|
@@ -3,54 +3,28 @@ module Taro::Types::Shared::Fields
|
|
3
3
|
# Field types are set using class name Strings. The respective type classes
|
4
4
|
# are evaluated lazily to allow for circular or recursive type references,
|
5
5
|
# and to avoid unnecessary autoloading of all types in dev/test envs.
|
6
|
-
def field(name, **
|
7
|
-
|
8
|
-
|
9
|
-
validate_no_override(name, defined_at:)
|
10
|
-
validate_options(name, defined_at:, **kwargs)
|
6
|
+
def field(name, **attributes)
|
7
|
+
attributes[:defined_at] ||= caller_locations(1..1)[0]
|
8
|
+
field_def = Taro::Types::FieldDef.new(name:, **attributes)
|
11
9
|
|
12
|
-
field_defs[name]
|
10
|
+
(prev = field_defs[name]) && raise(Taro::ArgumentError, <<~MSG)
|
11
|
+
field #{name} at #{field_def.defined_at}
|
12
|
+
previously defined at #{prev.defined_at}.
|
13
|
+
MSG
|
14
|
+
|
15
|
+
field_defs[name] = field_def
|
13
16
|
end
|
14
17
|
|
15
18
|
def fields
|
16
|
-
@fields ||=
|
19
|
+
@fields ||= field_defs.transform_values(&:evaluate)
|
17
20
|
end
|
18
21
|
|
19
22
|
private
|
20
23
|
|
21
|
-
def validate_name(name, defined_at:)
|
22
|
-
name.is_a?(Symbol) ||
|
23
|
-
raise(Taro::ArgumentError, "field name must be a Symbol, got #{name.class} at #{defined_at}")
|
24
|
-
end
|
25
|
-
|
26
|
-
def validate_options(name, defined_at:, **kwargs)
|
27
|
-
[true, false].include?(kwargs[:null]) ||
|
28
|
-
raise(Taro::ArgumentError, "null has to be specified as true or false for field #{name} at #{defined_at}")
|
29
|
-
|
30
|
-
c_keys = Taro::Types::Coercion.keys
|
31
|
-
(type_keys = (kwargs.keys & c_keys)).size == 1 ||
|
32
|
-
raise(Taro::ArgumentError, "exactly one of #{c_keys.join(', ')} must be given for field #{name} at #{defined_at}")
|
33
|
-
|
34
|
-
kwargs[type_keys.first].class == String ||
|
35
|
-
raise(Taro::ArgumentError, "#{type_keys.first} must be a String for field #{name} at #{defined_at}")
|
36
|
-
end
|
37
|
-
|
38
|
-
def validate_no_override(name, defined_at:)
|
39
|
-
prev = field_defs[name]
|
40
|
-
prev && raise(Taro::ArgumentError, "field #{name} at #{defined_at} previously defined at #{prev[:defined_at]}")
|
41
|
-
end
|
42
|
-
|
43
24
|
def field_defs
|
44
25
|
@field_defs ||= {}
|
45
26
|
end
|
46
27
|
|
47
|
-
def evaluate_field_defs
|
48
|
-
field_defs.transform_values do |field_def|
|
49
|
-
type = Taro::Types::Coercion.call(field_def)
|
50
|
-
Taro::Types::Field.new(**field_def.except(*Taro::Types::Coercion.keys), type:)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
28
|
def inherited(subclass)
|
55
29
|
subclass.instance_variable_set(:@field_defs, field_defs.dup)
|
56
30
|
super
|
@@ -3,8 +3,6 @@ module Taro::Types::Shared::ObjectCoercion
|
|
3
3
|
def coerce_input
|
4
4
|
self.class.fields.transform_values do |field|
|
5
5
|
field.value_for_input(object)
|
6
|
-
rescue Taro::Error => e
|
7
|
-
raise_enriched_coercion_error(e, field)
|
8
6
|
end
|
9
7
|
end
|
10
8
|
|
@@ -13,17 +11,6 @@ module Taro::Types::Shared::ObjectCoercion
|
|
13
11
|
object_is_hash = object.is_a?(Hash)
|
14
12
|
self.class.fields.transform_values do |field|
|
15
13
|
field.value_for_response(object, context: self, object_is_hash:)
|
16
|
-
rescue Taro::Error => e
|
17
|
-
raise_enriched_coercion_error(e, field)
|
18
14
|
end
|
19
15
|
end
|
20
|
-
|
21
|
-
def raise_enriched_coercion_error(error, field)
|
22
|
-
# The indentation is on purpose. These errors can be recursively rescued
|
23
|
-
# and re-raised by a tree of object types, which should be made apparent.
|
24
|
-
raise error.class, <<~MSG
|
25
|
-
Failed to read #{self.class.name} field `#{field.name}` from #{object.class}:
|
26
|
-
#{error.message.lines.map { |line| " #{line}" }.join}
|
27
|
-
MSG
|
28
|
-
end
|
29
16
|
end
|
@@ -5,12 +5,6 @@ module Taro::Types::Shared::OpenAPIName
|
|
5
5
|
@openapi_name ||= default_openapi_name
|
6
6
|
end
|
7
7
|
|
8
|
-
def openapi_name?
|
9
|
-
!!openapi_name
|
10
|
-
rescue Taro::Error
|
11
|
-
false
|
12
|
-
end
|
13
|
-
|
14
8
|
def openapi_name=(arg)
|
15
9
|
arg.nil? || arg.is_a?(String) ||
|
16
10
|
raise(Taro::ArgumentError, 'openapi_name must be a String')
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module Taro::Types::Shared::Rendering
|
2
2
|
# The `::render` method is intended for use in controllers.
|
3
3
|
# Overrides of this method must call super.
|
4
|
-
def render(object)
|
5
|
-
result =
|
6
|
-
|
4
|
+
def render(object, cache_attrs = {})
|
5
|
+
result = Taro::Cache.call(object, **cache_attrs) do
|
6
|
+
new(object).cached_coerce_response
|
7
|
+
end
|
8
|
+
self.last_render = [self.without_cache || self, result.__id__]
|
7
9
|
result
|
8
10
|
end
|
9
11
|
|
data/lib/taro/types/shared.rb
CHANGED
data/lib/taro/types.rb
CHANGED
data/lib/taro/version.rb
CHANGED
data/lib/taro.rb
CHANGED