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.
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|