taro 1.4.0 → 2.1.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 (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