taro 1.4.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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