taro 1.0.0 → 1.2.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 -1
- data/README.md +77 -14
- data/lib/taro/errors.rb +7 -1
- data/lib/taro/export/open_api_v3.rb +54 -23
- data/lib/taro/rails/active_declarations.rb +1 -1
- data/lib/taro/rails/declaration.rb +50 -9
- data/lib/taro/rails/generators/install_generator.rb +1 -1
- data/lib/taro/rails/generators/templates/errors_type.erb +15 -10
- data/lib/taro/rails/normalized_route.rb +8 -0
- data/lib/taro/rails/response_validation.rb +7 -57
- data/lib/taro/rails/response_validator.rb +109 -0
- data/lib/taro/rails/tasks/export.rake +5 -1
- data/lib/taro/rails.rb +1 -2
- data/lib/taro/types/base_type.rb +2 -0
- data/lib/taro/types/coercion.rb +28 -17
- data/lib/taro/types/enum_type.rb +2 -2
- data/lib/taro/types/field.rb +8 -16
- data/lib/taro/types/field_validation.rb +1 -1
- data/lib/taro/types/list_type.rb +4 -6
- data/lib/taro/types/object_types/free_form_type.rb +1 -0
- data/lib/taro/types/object_types/no_content_type.rb +1 -0
- data/lib/taro/types/object_types/page_info_type.rb +2 -0
- data/lib/taro/types/object_types/page_type.rb +15 -25
- data/lib/taro/types/scalar/iso8601_date_type.rb +1 -0
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +1 -0
- data/lib/taro/types/scalar/timestamp_type.rb +1 -0
- data/lib/taro/types/scalar/uuid_v4_type.rb +1 -0
- data/lib/taro/types/shared/deprecation.rb +3 -0
- data/lib/taro/types/shared/derived_types.rb +27 -0
- data/lib/taro/types/shared/errors.rb +3 -1
- data/lib/taro/types/shared/fields.rb +6 -5
- data/lib/taro/types/shared/item_type.rb +1 -0
- data/lib/taro/types/shared/object_coercion.rb +13 -0
- data/lib/taro/types/shared/openapi_name.rb +8 -6
- data/lib/taro/types/shared/rendering.rb +11 -25
- data/lib/taro/version.rb +1 -1
- data/tasks/benchmark.rake +1 -1
- metadata +7 -5
- data/lib/taro/types/shared/derivable_types.rb +0 -9
@@ -0,0 +1,109 @@
|
|
1
|
+
Taro::Rails::ResponseValidator = Struct.new(:controller, :declaration, :rendered) do
|
2
|
+
def self.call(*args)
|
3
|
+
new(*args).call
|
4
|
+
end
|
5
|
+
|
6
|
+
def call
|
7
|
+
if declared_return_type < Taro::Types::ScalarType
|
8
|
+
check_scalar
|
9
|
+
elsif declared_return_type < Taro::Types::ListType &&
|
10
|
+
declared_return_type.item_type < Taro::Types::ScalarType
|
11
|
+
check_scalar_array
|
12
|
+
elsif declared_return_type < Taro::Types::EnumType
|
13
|
+
check_enum
|
14
|
+
else
|
15
|
+
check_custom_type
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def declared_return_type
|
20
|
+
@declared_return_type ||= begin
|
21
|
+
return_type = declaration.returns[controller.status] ||
|
22
|
+
fail_with('No return type declared for this status.')
|
23
|
+
nesting ? return_type.fields.fetch(nesting).type : return_type
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fail_with(message)
|
28
|
+
raise Taro::ResponseError, <<~MSG
|
29
|
+
Response validation error for
|
30
|
+
#{controller.class}##{controller.action_name}, code #{controller.status}":
|
31
|
+
#{message}
|
32
|
+
MSG
|
33
|
+
end
|
34
|
+
|
35
|
+
# support `returns :some_nesting, type: 'SomeType'` (ad-hoc return type)
|
36
|
+
def nesting
|
37
|
+
@nesting ||= declaration.return_nestings[controller.status]
|
38
|
+
end
|
39
|
+
|
40
|
+
def denest_rendered
|
41
|
+
assert_rendered_is_a_hash
|
42
|
+
|
43
|
+
if rendered.key?(nesting)
|
44
|
+
rendered[nesting]
|
45
|
+
elsif rendered.key?(nesting.to_s)
|
46
|
+
rendered[nesting.to_s]
|
47
|
+
else
|
48
|
+
fail_with_nesting_error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def assert_rendered_is_a_hash
|
53
|
+
rendered.is_a?(Hash) || fail_with("Expected Hash, got #{rendered.class}.")
|
54
|
+
end
|
55
|
+
|
56
|
+
def fail_with_nesting_error
|
57
|
+
fail_with "Expected key :#{nesting}, got: #{rendered.keys}."
|
58
|
+
end
|
59
|
+
|
60
|
+
# For scalar and enum types, we want to support e.g. `render json: 42`,
|
61
|
+
# and not require using the type as in `BeautifulNumbersEnum.render(42)`.
|
62
|
+
def check_scalar(type = declared_return_type, value = subject)
|
63
|
+
case type.openapi_type
|
64
|
+
when :integer, :number then value.is_a?(Numeric)
|
65
|
+
when :string then value.is_a?(String) || value.is_a?(Symbol)
|
66
|
+
when :boolean then [true, false].include?(value)
|
67
|
+
end || fail_with("Expected a #{type.openapi_type}, got: #{value.class}.")
|
68
|
+
end
|
69
|
+
|
70
|
+
def subject
|
71
|
+
@subject ||= nesting ? denest_rendered : rendered
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_scalar_array
|
75
|
+
subject.is_a?(Array) || fail_with('Expected an Array.')
|
76
|
+
subject.empty? || check_scalar(declared_return_type.item_type, subject.first)
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_enum
|
80
|
+
# coercion checks non-emptyness + enum match
|
81
|
+
declared_return_type.new(subject).coerce_response
|
82
|
+
rescue Taro::Error => e
|
83
|
+
fail_with(e.message)
|
84
|
+
end
|
85
|
+
|
86
|
+
# For complex/object types, we ensure conformance by checking whether
|
87
|
+
# the type was used for rendering. This has performance benefits compared
|
88
|
+
# to going over the structure a second time.
|
89
|
+
def check_custom_type
|
90
|
+
# Ignore types without a specified structure.
|
91
|
+
return if declared_return_type <= Taro::Types::ObjectTypes::FreeFormType
|
92
|
+
return if declared_return_type <= Taro::Types::ObjectTypes::NoContentType
|
93
|
+
|
94
|
+
strict_check_custom_type
|
95
|
+
end
|
96
|
+
|
97
|
+
def strict_check_custom_type
|
98
|
+
used_type, rendered_object_id = declared_return_type.last_render
|
99
|
+
used_type&.<=(declared_return_type) || fail_with(<<~MSG)
|
100
|
+
Expected to use #{declared_return_type}.render, but the last type rendered
|
101
|
+
was: #{used_type || 'no type'}.
|
102
|
+
MSG
|
103
|
+
|
104
|
+
rendered_object_id == subject.__id__ || fail_with(<<~MSG)
|
105
|
+
#{declared_return_type}.render was called, but the result
|
106
|
+
of this call was not used in the response.
|
107
|
+
MSG
|
108
|
+
end
|
109
|
+
end
|
@@ -10,6 +10,10 @@ task 'taro:export' => :environment do
|
|
10
10
|
version: Taro.config.api_version,
|
11
11
|
)
|
12
12
|
|
13
|
-
data = export.
|
13
|
+
data = export.send("to_#{Taro.config.export_format}")
|
14
|
+
|
15
|
+
FileUtils.mkdir_p(File.dirname(Taro.config.export_path))
|
14
16
|
File.write(Taro.config.export_path, data)
|
17
|
+
|
18
|
+
puts "Exported #{Taro.config.api_name} to #{Taro.config.export_path}"
|
15
19
|
end
|
data/lib/taro/rails.rb
CHANGED
data/lib/taro/types/base_type.rb
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
Taro::Types::BaseType = Data.define(:object) do
|
10
10
|
require_relative "shared"
|
11
11
|
extend Taro::Types::Shared::AdditionalProperties
|
12
|
+
extend Taro::Types::Shared::Deprecation
|
13
|
+
extend Taro::Types::Shared::DerivedTypes
|
12
14
|
extend Taro::Types::Shared::Description
|
13
15
|
extend Taro::Types::Shared::OpenAPIName
|
14
16
|
extend Taro::Types::Shared::OpenAPIType
|
data/lib/taro/types/coercion.rb
CHANGED
@@ -1,42 +1,52 @@
|
|
1
1
|
module Taro::Types::Coercion
|
2
|
-
KEYS = %i[type array_of page_of].freeze
|
3
|
-
|
4
2
|
class << self
|
5
3
|
def call(arg)
|
6
4
|
validate_hash(arg)
|
7
5
|
from_hash(arg)
|
8
6
|
end
|
9
7
|
|
8
|
+
# Coercion keys can be expanded by the DerivedTypes module.
|
9
|
+
def keys
|
10
|
+
@keys ||= %i[type]
|
11
|
+
end
|
12
|
+
|
13
|
+
def derived_suffix
|
14
|
+
'_of'
|
15
|
+
end
|
16
|
+
|
10
17
|
private
|
11
18
|
|
12
19
|
def validate_hash(arg)
|
13
20
|
arg.is_a?(Hash) || raise(Taro::ArgumentError, <<~MSG)
|
14
|
-
Type coercion argument must be a Hash, got: #{arg.
|
21
|
+
Type coercion argument must be a Hash, got: #{arg.class}
|
15
22
|
MSG
|
16
23
|
|
17
|
-
types = arg.slice(*
|
24
|
+
types = arg.slice(*keys)
|
18
25
|
types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
|
19
|
-
Exactly one of
|
26
|
+
Exactly one of #{keys.join(', ')} must be given, got: #{types}
|
20
27
|
MSG
|
21
28
|
end
|
22
29
|
|
23
30
|
def from_hash(hash)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
31
|
+
keys.each do |key|
|
32
|
+
next unless (value = hash[key])
|
33
|
+
|
34
|
+
# e.g. `returns type: 'MyType'` -> MyType
|
35
|
+
return from_string(value) if key == :type
|
36
|
+
|
37
|
+
# DerivedTypes
|
38
|
+
# e.g. `returns array_of: 'MyType'` -> MyType.array
|
39
|
+
return from_string(value).send(key.to_s.chomp(derived_suffix))
|
32
40
|
end
|
41
|
+
|
42
|
+
raise NotImplementedError, "Unsupported type coercion #{hash}"
|
33
43
|
end
|
34
44
|
|
35
45
|
def from_string(arg)
|
36
46
|
shortcuts[arg] || from_class(Object.const_get(arg.to_s))
|
37
47
|
rescue NameError
|
38
48
|
raise Taro::ArgumentError, <<~MSG
|
39
|
-
|
49
|
+
No such type: #{arg}. It should be a type-class name
|
40
50
|
or one of #{shortcuts.keys.map(&:inspect).join(', ')}.
|
41
51
|
MSG
|
42
52
|
end
|
@@ -56,15 +66,16 @@ module Taro::Types::Coercion
|
|
56
66
|
@shortcuts ||= {
|
57
67
|
# rubocop:disable Layout/HashAlignment - buggy cop
|
58
68
|
'Boolean' => Taro::Types::Scalar::BooleanType,
|
69
|
+
'Date' => Taro::Types::Scalar::ISO8601DateType,
|
70
|
+
'DateTime' => Taro::Types::Scalar::ISO8601DateTimeType,
|
59
71
|
'Float' => Taro::Types::Scalar::FloatType,
|
60
72
|
'FreeForm' => Taro::Types::ObjectTypes::FreeFormType,
|
61
73
|
'Integer' => Taro::Types::Scalar::IntegerType,
|
74
|
+
'NoContent' => Taro::Types::ObjectTypes::NoContentType,
|
62
75
|
'String' => Taro::Types::Scalar::StringType,
|
76
|
+
'Time' => Taro::Types::Scalar::ISO8601DateTimeType,
|
63
77
|
'Timestamp' => Taro::Types::Scalar::TimestampType,
|
64
78
|
'UUID' => Taro::Types::Scalar::UUIDv4Type,
|
65
|
-
'Date' => Taro::Types::Scalar::ISO8601DateType,
|
66
|
-
'Time' => Taro::Types::Scalar::ISO8601DateTimeType,
|
67
|
-
'DateTime' => Taro::Types::Scalar::ISO8601DateTimeType,
|
68
79
|
# rubocop:enable Layout/HashAlignment - buggy cop
|
69
80
|
}.freeze
|
70
81
|
end
|
data/lib/taro/types/enum_type.rb
CHANGED
@@ -18,7 +18,7 @@ class Taro::Types::EnumType < Taro::Types::BaseType
|
|
18
18
|
if self.class.values.include?(value)
|
19
19
|
value
|
20
20
|
else
|
21
|
-
input_error("must be
|
21
|
+
input_error("must be #{self.class.values.map(&:inspect).join(' or ')}")
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -28,7 +28,7 @@ class Taro::Types::EnumType < Taro::Types::BaseType
|
|
28
28
|
if self.class.values.include?(value)
|
29
29
|
value
|
30
30
|
else
|
31
|
-
response_error("must be
|
31
|
+
response_error("must be #{self.class.values.map(&:inspect).join(' or ')}")
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
data/lib/taro/types/field.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require_relative 'field_validation'
|
2
2
|
|
3
|
-
Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc) do
|
3
|
+
Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc, :deprecated) do
|
4
4
|
include Taro::Types::FieldValidation
|
5
5
|
|
6
|
-
def initialize(name:, type:, null:, method: name, default: :none, enum: nil, defined_at: nil, desc: nil)
|
6
|
+
def initialize(name:, type:, null:, method: name, default: :none, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
|
7
7
|
enum = coerce_to_enum(enum)
|
8
|
-
super(name:, type:, null:, method:, default:, enum:, defined_at:, desc:)
|
8
|
+
super(name:, type:, null:, method:, default:, enum:, defined_at:, desc:, deprecated:)
|
9
9
|
end
|
10
10
|
|
11
11
|
def value_for_input(object)
|
@@ -40,14 +40,15 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def retrieve_response_value(object, context, object_is_hash)
|
43
|
-
if
|
44
|
-
retrieve_hash_value(object)
|
45
|
-
elsif context&.resolve?(method)
|
43
|
+
if context&.resolve?(method)
|
46
44
|
context.public_send(method)
|
45
|
+
elsif object_is_hash
|
46
|
+
retrieve_hash_value(object)
|
47
47
|
elsif object.respond_to?(method, true)
|
48
48
|
object.public_send(method)
|
49
49
|
else
|
50
|
-
|
50
|
+
# Note that the ObjectCoercion module rescues this and adds context.
|
51
|
+
raise Taro::ResponseError, "No such method or resolver `:#{method}`."
|
51
52
|
end
|
52
53
|
end
|
53
54
|
|
@@ -65,14 +66,5 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
|
|
65
66
|
|
66
67
|
type_obj = type.new(value)
|
67
68
|
from_input ? type_obj.coerce_input : type_obj.coerce_response
|
68
|
-
rescue Taro::Error => e
|
69
|
-
raise e.class, "#{e.message}, after using method/key `:#{method}` to resolve field `#{name}`"
|
70
|
-
end
|
71
|
-
|
72
|
-
def raise_response_coercion_error(object)
|
73
|
-
raise Taro::ResponseError, <<~MSG
|
74
|
-
Failed to coerce value #{object.inspect} for field `#{name}` using method/key `:#{method}`.
|
75
|
-
It is not a valid #{type} value.
|
76
|
-
MSG
|
77
69
|
end
|
78
70
|
end
|
@@ -18,7 +18,7 @@ module Taro::Types::FieldValidation
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def validate_enum_inclusion(value, for_input)
|
21
|
-
return if enum.nil? || enum.include?(value)
|
21
|
+
return if enum.nil? || null && value.nil? || enum.include?(value)
|
22
22
|
|
23
23
|
raise for_input ? Taro::InputError : Taro::ResponseError, <<~MSG
|
24
24
|
Field #{name} has an invalid value #{value.inspect} (expected one of #{enum.inspect})
|
data/lib/taro/types/list_type.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
# Unlike other types, this one should not be manually inherited from,
|
3
3
|
# but is used indirectly via `array_of: SomeType`.
|
4
4
|
class Taro::Types::ListType < Taro::Types::BaseType
|
5
|
-
extend Taro::Types::Shared::DerivableType
|
6
5
|
extend Taro::Types::Shared::ItemType
|
7
6
|
|
8
7
|
self.openapi_type = :array
|
@@ -20,11 +19,10 @@ class Taro::Types::ListType < Taro::Types::BaseType
|
|
20
19
|
item_type = self.class.item_type
|
21
20
|
object.map { |el| item_type.new(el).coerce_response }
|
22
21
|
end
|
23
|
-
end
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
def self.array
|
28
|
-
Taro::Types::ListType.for(self)
|
23
|
+
def self.default_openapi_name
|
24
|
+
"#{item_type.openapi_name}_List"
|
29
25
|
end
|
26
|
+
|
27
|
+
define_derived_type :array, 'Taro::Types::ListType'
|
30
28
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class Taro::Types::ObjectTypes::FreeFormType < Taro::Types::ObjectType
|
2
2
|
self.desc = 'An arbitrary, unvalidated Hash or JSON object. Use with care.'
|
3
3
|
self.additional_properties = true
|
4
|
+
self.openapi_name = 'FreeForm'
|
4
5
|
|
5
6
|
def coerce_input
|
6
7
|
object.is_a?(Hash) && object || input_error('must be a Hash')
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class Taro::Types::ObjectTypes::PageInfoType < Taro::Types::ObjectType
|
2
|
+
self.openapi_name = 'PageInfo'
|
3
|
+
|
2
4
|
field :has_previous_page, type: 'Boolean', null: false, desc: 'Whether there is a previous page of results'
|
3
5
|
field :has_next_page, type: 'Boolean', null: false, desc: 'Whether there is another page of results'
|
4
6
|
field :start_cursor, type: 'String', null: true, desc: 'The first cursor in the current page of results (null if zero results)'
|
@@ -4,42 +4,32 @@
|
|
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::
|
8
|
-
extend Taro::Types::Shared::DerivableType
|
7
|
+
class Taro::Types::ObjectTypes::PageType < Taro::Types::ObjectType
|
9
8
|
extend Taro::Types::Shared::ItemType
|
10
9
|
|
10
|
+
def self.derive_from(from_type)
|
11
|
+
super
|
12
|
+
field(:page, array_of: from_type.name, null: false)
|
13
|
+
field(:page_info, type: 'Taro::Types::ObjectTypes::PageInfoType', null: false)
|
14
|
+
end
|
15
|
+
|
11
16
|
def coerce_input
|
12
17
|
input_error 'PageTypes cannot be used as input types'
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
20
|
+
def self.render(relation, after:, limit: 20, order_by: nil, order: nil)
|
21
|
+
result = RailsCursorPagination::Paginator.new(
|
22
|
+
relation, limit:, order_by:, order:, after:
|
18
23
|
).fetch
|
19
|
-
coerce_paginated_list(list)
|
20
|
-
end
|
21
24
|
|
22
|
-
|
23
|
-
item_type = self.class.item_type
|
24
|
-
items = list[:page].map do |item|
|
25
|
-
item_type.new(item[:data]).coerce_response
|
26
|
-
end
|
25
|
+
result[:page].map! { |el| el.fetch(:data) }
|
27
26
|
|
28
|
-
|
29
|
-
self.class.items_key => items,
|
30
|
-
page_info: Taro::Types::ObjectTypes::PageInfoType.new(list[:page_info]).coerce_response,
|
31
|
-
}
|
27
|
+
super(result)
|
32
28
|
end
|
33
29
|
|
34
|
-
|
35
|
-
|
36
|
-
:page
|
30
|
+
def self.default_openapi_name
|
31
|
+
"#{item_type.openapi_name}_Page"
|
37
32
|
end
|
38
|
-
end
|
39
33
|
|
40
|
-
|
41
|
-
class Taro::Types::BaseType
|
42
|
-
def self.page
|
43
|
-
Taro::Types::ObjectTypes::PageType.for(self)
|
44
|
-
end
|
34
|
+
define_derived_type :page, 'Taro::Types::ObjectTypes::PageType'
|
45
35
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
class Taro::Types::Scalar::ISO8601DateTimeType < Taro::Types::ScalarType
|
2
2
|
self.desc = 'Represents a time as DateTime in ISO8601 format.'
|
3
|
+
self.openapi_name = 'ISO8601DateTime'
|
3
4
|
self.openapi_type = :string
|
4
5
|
|
5
6
|
PATTERN = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(Z|[+-](0[0-9]|1[0-4]):[0-5]\d)\z/
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Taro::Types::Shared::DerivedTypes
|
2
|
+
# Adds `name` as a method to all type classes and adds
|
3
|
+
# `name`_of as a supported key to the Coercion module.
|
4
|
+
# When `name` is called on a type class T, it returns a new subclass
|
5
|
+
# S inheriting from `type` and passes T to S::derive_from.
|
6
|
+
def define_derived_type(name, type)
|
7
|
+
root = Taro::Types::BaseType
|
8
|
+
raise ArgumentError, "#{name} is already in use" if root.respond_to?(name)
|
9
|
+
|
10
|
+
ckey = :"#{name}#{Taro::Types::Coercion.derived_suffix}"
|
11
|
+
ckeys = Taro::Types::Coercion.keys
|
12
|
+
raise ArgumentError, "#{ckey} is already in use" if ckeys.include?(ckey)
|
13
|
+
|
14
|
+
root.define_singleton_method(name) do
|
15
|
+
derived_types[type] ||= begin
|
16
|
+
type_class = Taro::Types::Coercion.call(type:)
|
17
|
+
Class.new(type_class).tap { |t| t.derive_from(self) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
ckeys << ckey
|
22
|
+
end
|
23
|
+
|
24
|
+
def derived_types
|
25
|
+
@derived_types ||= {}
|
26
|
+
end
|
27
|
+
end
|
@@ -8,6 +8,8 @@ module Taro::Types::Shared::Errors
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def coerce_error_message(msg)
|
11
|
-
|
11
|
+
type_desc = (self.class.name || self.class.superclass.name)
|
12
|
+
.sub(/^Taro::Types::.*?([^:]+)Type$/, '\1')
|
13
|
+
"#{object.class} is not valid as #{type_desc}: #{msg}"
|
12
14
|
end
|
13
15
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
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
|
-
# and to avoid unnecessary
|
5
|
+
# and to avoid unnecessary autoloading of all types in dev/test envs.
|
6
6
|
def field(name, **kwargs)
|
7
7
|
defined_at = kwargs[:defined_at] || caller_locations(1..1)[0]
|
8
8
|
validate_name(name, defined_at:)
|
@@ -27,11 +27,12 @@ module Taro::Types::Shared::Fields
|
|
27
27
|
[true, false].include?(kwargs[:null]) ||
|
28
28
|
raise(Taro::ArgumentError, "null has to be specified as true or false for field #{name} at #{defined_at}")
|
29
29
|
|
30
|
-
|
31
|
-
|
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}")
|
32
33
|
|
33
34
|
kwargs[type_keys.first].class == String ||
|
34
|
-
raise(Taro::ArgumentError, "#{
|
35
|
+
raise(Taro::ArgumentError, "#{type_keys.first} must be a String for field #{name} at #{defined_at}")
|
35
36
|
end
|
36
37
|
|
37
38
|
def validate_no_override(name, defined_at:)
|
@@ -46,7 +47,7 @@ module Taro::Types::Shared::Fields
|
|
46
47
|
def evaluate_field_defs
|
47
48
|
field_defs.transform_values do |field_def|
|
48
49
|
type = Taro::Types::Coercion.call(field_def)
|
49
|
-
Taro::Types::Field.new(**field_def.except(*Taro::Types::Coercion
|
50
|
+
Taro::Types::Field.new(**field_def.except(*Taro::Types::Coercion.keys), type:)
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
@@ -3,6 +3,8 @@ 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)
|
6
8
|
end
|
7
9
|
end
|
8
10
|
|
@@ -11,6 +13,17 @@ module Taro::Types::Shared::ObjectCoercion
|
|
11
13
|
object_is_hash = object.is_a?(Hash)
|
12
14
|
self.class.fields.transform_values do |field|
|
13
15
|
field.value_for_response(object, context: self, object_is_hash:)
|
16
|
+
rescue Taro::Error => e
|
17
|
+
raise_enriched_coercion_error(e, field)
|
14
18
|
end
|
15
19
|
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
|
16
29
|
end
|
@@ -5,13 +5,19 @@ 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
|
+
|
8
14
|
def openapi_name=(arg)
|
9
15
|
arg.nil? || arg.is_a?(String) ||
|
10
16
|
raise(Taro::ArgumentError, 'openapi_name must be a String')
|
11
17
|
@openapi_name = arg
|
12
18
|
end
|
13
19
|
|
14
|
-
def default_openapi_name
|
20
|
+
def default_openapi_name
|
15
21
|
if self < Taro::Types::EnumType ||
|
16
22
|
self < Taro::Types::InputType ||
|
17
23
|
self < Taro::Types::ObjectType
|
@@ -19,12 +25,8 @@ module Taro::Types::Shared::OpenAPIName
|
|
19
25
|
raise(Taro::Error, 'openapi_name must be set for anonymous type classes')
|
20
26
|
elsif self < Taro::Types::ScalarType
|
21
27
|
openapi_type
|
22
|
-
elsif self < Taro::Types::ListType
|
23
|
-
"#{item_type.openapi_name}_List"
|
24
|
-
elsif self < Taro::Types::ObjectTypes::PageType
|
25
|
-
"#{item_type.openapi_name}_Page"
|
26
28
|
else
|
27
|
-
raise
|
29
|
+
raise Taro::Error, 'no default_openapi_name implemented for this type'
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
@@ -1,36 +1,22 @@
|
|
1
|
-
# The `::render` method is intended for use in controllers.
|
2
|
-
# Special types (e.g. PageType) may accept kwargs for `#coerce_response`.
|
3
1
|
module Taro::Types::Shared::Rendering
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
MSG
|
10
|
-
end
|
11
|
-
|
12
|
-
result = new(object).coerce_response(**opts)
|
13
|
-
|
14
|
-
# Only mark this as the used type if coercion worked so that
|
15
|
-
# rescue_from can be used to render another type.
|
16
|
-
self.rendering = self
|
17
|
-
|
2
|
+
# The `::render` method is intended for use in controllers.
|
3
|
+
# Overrides of this method must call super.
|
4
|
+
def render(object)
|
5
|
+
result = new(object).coerce_response
|
6
|
+
self.last_render = [self, result.__id__]
|
18
7
|
result
|
19
8
|
end
|
20
9
|
|
21
|
-
def
|
22
|
-
ActiveSupport::IsolatedExecutionState[:
|
23
|
-
end
|
24
|
-
|
25
|
-
def rendering
|
26
|
-
ActiveSupport::IsolatedExecutionState[:taro_type_rendering]
|
10
|
+
def last_render=(info)
|
11
|
+
ActiveSupport::IsolatedExecutionState[:taro_last_render] = info
|
27
12
|
end
|
28
13
|
|
29
|
-
def
|
30
|
-
ActiveSupport::IsolatedExecutionState[:
|
14
|
+
def last_render
|
15
|
+
ActiveSupport::IsolatedExecutionState[:taro_last_render]
|
31
16
|
end
|
32
17
|
|
18
|
+
# get the last used type for assertions in tests/specs
|
33
19
|
def used_in_response
|
34
|
-
|
20
|
+
last_render.to_a.first
|
35
21
|
end
|
36
22
|
end
|
data/lib/taro/version.rb
CHANGED