taro 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/README.md +77 -14
  4. data/lib/taro/errors.rb +7 -1
  5. data/lib/taro/export/open_api_v3.rb +54 -23
  6. data/lib/taro/rails/active_declarations.rb +1 -1
  7. data/lib/taro/rails/declaration.rb +50 -9
  8. data/lib/taro/rails/generators/install_generator.rb +1 -1
  9. data/lib/taro/rails/generators/templates/errors_type.erb +15 -10
  10. data/lib/taro/rails/normalized_route.rb +8 -0
  11. data/lib/taro/rails/response_validation.rb +7 -57
  12. data/lib/taro/rails/response_validator.rb +109 -0
  13. data/lib/taro/rails/tasks/export.rake +5 -1
  14. data/lib/taro/rails.rb +1 -2
  15. data/lib/taro/types/base_type.rb +2 -0
  16. data/lib/taro/types/coercion.rb +28 -17
  17. data/lib/taro/types/enum_type.rb +2 -2
  18. data/lib/taro/types/field.rb +8 -16
  19. data/lib/taro/types/field_validation.rb +1 -1
  20. data/lib/taro/types/list_type.rb +4 -6
  21. data/lib/taro/types/object_types/free_form_type.rb +1 -0
  22. data/lib/taro/types/object_types/no_content_type.rb +1 -0
  23. data/lib/taro/types/object_types/page_info_type.rb +2 -0
  24. data/lib/taro/types/object_types/page_type.rb +15 -25
  25. data/lib/taro/types/scalar/iso8601_date_type.rb +1 -0
  26. data/lib/taro/types/scalar/iso8601_datetime_type.rb +1 -0
  27. data/lib/taro/types/scalar/timestamp_type.rb +1 -0
  28. data/lib/taro/types/scalar/uuid_v4_type.rb +1 -0
  29. data/lib/taro/types/shared/deprecation.rb +3 -0
  30. data/lib/taro/types/shared/derived_types.rb +27 -0
  31. data/lib/taro/types/shared/errors.rb +3 -1
  32. data/lib/taro/types/shared/fields.rb +6 -5
  33. data/lib/taro/types/shared/item_type.rb +1 -0
  34. data/lib/taro/types/shared/object_coercion.rb +13 -0
  35. data/lib/taro/types/shared/openapi_name.rb +8 -6
  36. data/lib/taro/types/shared/rendering.rb +11 -25
  37. data/lib/taro/version.rb +1 -1
  38. data/tasks/benchmark.rake +1 -1
  39. metadata +7 -5
  40. 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.result.send("to_#{Taro.config.export_format}")
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
@@ -12,7 +12,6 @@ module Taro::Rails
12
12
  buffered_declarations.clear
13
13
  declarations_map.clear
14
14
  RouteFinder.clear_cache
15
- Taro::Types::BaseType.rendering = nil
16
- Taro::Types::BaseType.used_in_response = nil
15
+ Taro::Types::BaseType.last_render = nil
17
16
  end
18
17
  end
@@ -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
@@ -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.inspect} (#{arg.class})
21
+ Type coercion argument must be a Hash, got: #{arg.class}
15
22
  MSG
16
23
 
17
- types = arg.slice(*KEYS)
24
+ types = arg.slice(*keys)
18
25
  types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
19
- Exactly one of type, array_of, or page_of must be given, got: #{types}
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
- if hash[:type]
25
- from_string(hash[:type])
26
- elsif (inner_type = hash[:array_of])
27
- from_string(inner_type).array
28
- elsif (inner_type = hash[:page_of])
29
- from_string(inner_type).page
30
- else
31
- raise NotImplementedError, 'Unsupported type coercion'
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
- Unsupported type: #{arg}. It should be a type-class name
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
@@ -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 one of #{self.class.values}")
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 one of #{self.class.values}")
31
+ response_error("must be #{self.class.values.map(&:inspect).join(' or ')}")
32
32
  end
33
33
  end
34
34
 
@@ -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 object_is_hash
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
- raise_response_coercion_error(object)
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})
@@ -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
- # add shortcut to other types
26
- class Taro::Types::BaseType
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,5 +1,6 @@
1
1
  class Taro::Types::ObjectTypes::NoContentType < Taro::Types::ObjectType
2
2
  self.desc = 'An empty response'
3
+ self.openapi_name = 'NoContent'
3
4
 
4
5
  # render takes no arguments in this case
5
6
  def self.render
@@ -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::BaseType
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 coerce_response(after:, limit: 20, order_by: nil, order: nil)
16
- list = RailsCursorPagination::Paginator.new(
17
- object, limit:, order_by:, order:, after:
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
- def coerce_paginated_list(list)
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
- # support overrides, e.g. based on item_type
35
- def self.items_key
36
- :page
30
+ def self.default_openapi_name
31
+ "#{item_type.openapi_name}_Page"
37
32
  end
38
- end
39
33
 
40
- # add shortcut to other types
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::ISO8601DateType < Taro::Types::ScalarType
2
2
  self.desc = 'Represents a time as Date in ISO8601 format.'
3
+ self.openapi_name = 'ISO8601Date'
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])\z/
@@ -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/
@@ -1,5 +1,6 @@
1
1
  class Taro::Types::Scalar::TimestampType < Taro::Types::ScalarType
2
2
  self.desc = 'Represents a time as Time on the server side and UNIX timestamp (integer) on the client side.'
3
+ self.openapi_name = 'Timestamp'
3
4
  self.openapi_type = :integer
4
5
 
5
6
  def coerce_input
@@ -1,5 +1,6 @@
1
1
  class Taro::Types::Scalar::UUIDv4Type < Taro::Types::ScalarType
2
2
  self.desc = "A UUID v4 string"
3
+ self.openapi_name = 'UUIDv4'
3
4
  self.openapi_type = :string
4
5
 
5
6
  PATTERN = /\A\h{8}-?(?:\h{4}-?){3}\h{12}\z/
@@ -0,0 +1,3 @@
1
+ module Taro::Types::Shared::Deprecation
2
+ attr_accessor :deprecated
3
+ end
@@ -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
- "#{object.inspect} (#{object.class}) is not valid as #{self.class}: #{msg}"
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 eager loading of all types in dev/test envs.
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
- (type_keys = (kwargs.keys & Taro::Types::Coercion::KEYS)).size == 1 ||
31
- raise(Taro::ArgumentError, "exactly one of type, array_of, or page_of must be given for field #{name} at #{defined_at}")
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, "#{type_key} must be a String for field #{name} at #{defined_at}")
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::KEYS), type:)
50
+ Taro::Types::Field.new(**field_def.except(*Taro::Types::Coercion.keys), type:)
50
51
  end
51
52
  end
52
53
 
@@ -6,6 +6,7 @@ module Taro::Types::Shared::ItemType
6
6
  item_type.nil? || new_type == item_type || raise_mixed_types(new_type)
7
7
  @item_type = new_type
8
8
  end
9
+ alias_method :derive_from, :item_type=
9
10
 
10
11
  def raise_mixed_types(new_type)
11
12
  raise Taro::ArgumentError, <<~MSG
@@ -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 # rubocop:disable Metrics
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 NotImplementedError, 'no default_openapi_name for this type'
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
- def render(object, opts = {})
5
- if (prev = rendering)
6
- raise Taro::RuntimeError, <<~MSG
7
- Type.render should only be called once per request.
8
- (First called on #{prev}, then on #{self}.)
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 rendering=(value)
22
- ActiveSupport::IsolatedExecutionState[:taro_type_rendering] = value
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 used_in_response=(value)
30
- ActiveSupport::IsolatedExecutionState[:taro_type_used_in_response] = value
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
- ActiveSupport::IsolatedExecutionState[:taro_type_used_in_response]
20
+ last_render.to_a.first
35
21
  end
36
22
  end
data/lib/taro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # :nocov:
2
2
  module Taro
3
- VERSION = "1.0.0"
3
+ VERSION = "1.2.0"
4
4
  end
data/tasks/benchmark.rake CHANGED
@@ -13,7 +13,7 @@ task :benchmark do
13
13
  field :version, type: 'Float', null: false
14
14
  end
15
15
 
16
- type = Taro::Types::ListType.for(item_type)
16
+ type = item_type.array
17
17
 
18
18
  # 143.889k (± 2.7%) i/s - 723.816k in 5.034247s
19
19
  Benchmark.ips do |x|