taro 1.4.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +138 -60
  4. data/lib/taro/cache.rb +14 -0
  5. data/lib/taro/common_returns.rb +31 -0
  6. data/lib/taro/declaration.rb +82 -0
  7. data/lib/taro/declarations.rb +34 -0
  8. data/lib/taro/errors.rb +15 -2
  9. data/lib/taro/export/base.rb +1 -1
  10. data/lib/taro/export/open_api_v3.rb +20 -23
  11. data/lib/taro/export.rb +1 -1
  12. data/lib/taro/none.rb +2 -0
  13. data/lib/taro/rails/active_declarations.rb +2 -10
  14. data/lib/taro/rails/declaration.rb +9 -114
  15. data/lib/taro/rails/declaration_buffer.rb +2 -1
  16. data/lib/taro/rails/dsl.rb +13 -6
  17. data/lib/taro/rails/generators/install_generator.rb +1 -1
  18. data/lib/taro/rails/generators/templates/errors_type.erb +1 -1
  19. data/lib/taro/rails/generators/templates/response_type.erb +4 -0
  20. data/lib/taro/rails/generators.rb +1 -1
  21. data/lib/taro/rails/normalized_route.rb +20 -38
  22. data/lib/taro/rails/param_parsing.rb +5 -3
  23. data/lib/taro/rails/railtie.rb +4 -0
  24. data/lib/taro/rails/response_validator.rb +53 -52
  25. data/lib/taro/rails/route_finder.rb +5 -7
  26. data/lib/taro/rails/tasks/export.rake +10 -9
  27. data/lib/taro/rails.rb +2 -3
  28. data/lib/taro/return_def.rb +43 -0
  29. data/lib/taro/route.rb +32 -0
  30. data/lib/taro/status_code.rb +16 -0
  31. data/lib/taro/types/base_type.rb +7 -1
  32. data/lib/taro/types/coercion.rb +2 -2
  33. data/lib/taro/types/enum_type.rb +1 -1
  34. data/lib/taro/types/field.rb +17 -5
  35. data/lib/taro/types/field_def.rb +62 -0
  36. data/lib/taro/types/field_validation.rb +4 -6
  37. data/lib/taro/types/input_type.rb +4 -9
  38. data/lib/taro/types/list_type.rb +1 -1
  39. data/lib/taro/types/nested_response_type.rb +16 -0
  40. data/lib/taro/types/object_type.rb +2 -7
  41. data/lib/taro/types/object_types/no_content_type.rb +1 -5
  42. data/lib/taro/types/object_types/page_info_type.rb +1 -1
  43. data/lib/taro/types/object_types/page_type.rb +1 -5
  44. data/lib/taro/types/response_type.rb +8 -0
  45. data/lib/taro/types/scalar/integer_param_type.rb +15 -0
  46. data/lib/taro/types/scalar_type.rb +1 -1
  47. data/lib/taro/types/shared/caching.rb +30 -0
  48. data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
  49. data/lib/taro/types/shared/derived_types.rb +34 -15
  50. data/lib/taro/types/shared/equivalence.rb +14 -0
  51. data/lib/taro/types/shared/errors.rb +8 -8
  52. data/lib/taro/types/shared/fields.rb +10 -36
  53. data/lib/taro/types/shared/name.rb +14 -0
  54. data/lib/taro/types/shared/object_coercion.rb +0 -13
  55. data/lib/taro/types/shared/openapi_name.rb +0 -6
  56. data/lib/taro/types/shared/rendering.rb +5 -3
  57. data/lib/taro/types/shared.rb +1 -1
  58. data/lib/taro/types.rb +1 -1
  59. data/lib/taro/version.rb +1 -1
  60. data/lib/taro.rb +6 -1
  61. metadata +19 -3
@@ -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
- Taro::Types::BaseType = Data.define(:object) do
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
@@ -64,7 +64,7 @@ module Taro::Types::Coercion
64
64
  require 'date'
65
65
  def shortcuts
66
66
  @shortcuts ||= {
67
- # rubocop:disable Layout/HashAlignment - buggy cop
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 - buggy cop
79
+ # rubocop:enable Layout/HashAlignment
80
80
  }.freeze
81
81
  end
82
82
  end
@@ -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).coerce_response
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
@@ -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: :none, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
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?(:none)
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
- # Note that the ObjectCoercion module rescues this and adds context.
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.coerce_response
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
- raise for_input ? Taro::InputError : Taro::ResponseError, <<~MSG
16
- Field #{name} is not nullable (got #{value.inspect})
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
- raise for_input ? Taro::InputError : Taro::ResponseError, <<~MSG
24
- Field #{name} has an invalid value #{value.inspect} (expected one of #{enum.inspect})
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
- # Abstract base class for input types, i.e. types without response rendering.
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 'InputTypes cannot be used as response types'
6
+ response_error "#{self.class.name} is an InputType and cannot be used as response type"
12
7
  end
13
8
  end
@@ -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).coerce_response }
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
- Dir[File.join(__dir__, 'object_types', '**', '*.rb')].each { |f| require f }
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::ObjectType
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::ObjectType
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::ObjectType
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
@@ -4,5 +4,5 @@ class Taro::Types::ScalarType < Taro::Types::BaseType
4
4
  end
5
5
 
6
6
  module Taro::Types::Scalar
7
- Dir[File.join(__dir__, 'scalar', '**', '*.rb')].each { |f| require f }
7
+ Dir[File.join(__dir__, 'scalar', '**', '*.rb')].each { |f| require_relative f }
8
8
  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 [:object, :pattern].include?(name)
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 ![:coerce_input, :coerce_response].include?(name) &&
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
- # `name`_of as a supported key to the Coercion module.
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, type)
7
- root = Taro::Types::BaseType
8
- raise ArgumentError, "#{name} is already in use" if root.respond_to?(name)
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
- 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)
11
+ def derived_types
12
+ Taro::Types::Shared::DerivedTypes.map[self] ||= {}
13
+ end
13
14
 
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
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
- ckeys << ckey
27
+ Taro::Types::Coercion.keys << new_key
22
28
  end
23
29
 
24
- def derived_types
25
- @derived_types ||= {}
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, coerce_error_message(msg)
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, coerce_error_message(msg)
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
- 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}"
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, **kwargs)
7
- defined_at = kwargs[:defined_at] || caller_locations(1..1)[0]
8
- validate_name(name, defined_at:)
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] = { name:, defined_at:, **kwargs }
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 ||= evaluate_field_defs
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
@@ -0,0 +1,14 @@
1
+ module Taro::Types::Shared::Name
2
+ def define_name(name)
3
+ instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
4
+ def name
5
+ #{name.inspect}
6
+ end
7
+ alias to_s name
8
+
9
+ def inspect
10
+ "#<#{name}>"
11
+ end
12
+ RUBY
13
+ end
14
+ end
@@ -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 = new(object).coerce_response
6
- self.last_render = [self, result.__id__]
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
 
@@ -1,3 +1,3 @@
1
1
  module Taro::Types::Shared
2
- Dir[File.join(__dir__, "shared", "*.rb")].each { |f| require f }
2
+ Dir[File.join(__dir__, "shared", "*.rb")].each { |f| require_relative f }
3
3
  end
data/lib/taro/types.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Taro::Types
2
- Dir[File.join(__dir__, "types", "*.rb")].each { |f| require f }
2
+ Dir[File.join(__dir__, "types", "*.rb")].each { |f| require_relative f }
3
3
  end
data/lib/taro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # :nocov:
2
2
  module Taro
3
- VERSION = "1.4.0"
3
+ VERSION = "2.1.0"
4
4
  end
data/lib/taro.rb CHANGED
@@ -1,3 +1,8 @@
1
1
  module Taro
2
- Dir[File.join(__dir__, "taro", "*.rb")].each { |f| require f }
2
+ Dir[File.join(__dir__, "taro", "*.rb")].each { |f| require_relative f }
3
+
4
+ def self.reset
5
+ declarations.reset
6
+ Taro::Types::BaseType.last_render = nil
7
+ end
3
8
  end