taro 0.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/CHANGELOG.md +10 -0
  5. data/README.md +257 -1
  6. data/Rakefile +11 -0
  7. data/lib/taro/config.rb +22 -0
  8. data/lib/taro/errors.rb +12 -0
  9. data/lib/taro/export/base.rb +29 -0
  10. data/lib/taro/export/open_api_v3.rb +190 -0
  11. data/lib/taro/export.rb +3 -0
  12. data/lib/taro/rails/active_declarations.rb +19 -0
  13. data/lib/taro/rails/declaration.rb +118 -0
  14. data/lib/taro/rails/declaration_buffer.rb +24 -0
  15. data/lib/taro/rails/dsl.rb +18 -0
  16. data/lib/taro/rails/generators/install_generator.rb +19 -0
  17. data/lib/taro/rails/generators/templates/enum_type.erb +4 -0
  18. data/lib/taro/rails/generators/templates/error_type.erb +10 -0
  19. data/lib/taro/rails/generators/templates/errors_type.erb +25 -0
  20. data/lib/taro/rails/generators/templates/input_type.erb +4 -0
  21. data/lib/taro/rails/generators/templates/no_content_type.erb +4 -0
  22. data/lib/taro/rails/generators/templates/object_type.erb +4 -0
  23. data/lib/taro/rails/generators/templates/scalar_type.erb +4 -0
  24. data/lib/taro/rails/generators.rb +3 -0
  25. data/lib/taro/rails/normalized_route.rb +29 -0
  26. data/lib/taro/rails/param_parsing.rb +19 -0
  27. data/lib/taro/rails/railtie.rb +15 -0
  28. data/lib/taro/rails/response_validation.rb +13 -0
  29. data/lib/taro/rails/response_validator.rb +109 -0
  30. data/lib/taro/rails/route_finder.rb +35 -0
  31. data/lib/taro/rails/tasks/export.rake +15 -0
  32. data/lib/taro/rails.rb +17 -0
  33. data/lib/taro/types/base_type.rb +17 -0
  34. data/lib/taro/types/coercion.rb +73 -0
  35. data/lib/taro/types/enum_type.rb +43 -0
  36. data/lib/taro/types/field.rb +78 -0
  37. data/lib/taro/types/field_validation.rb +27 -0
  38. data/lib/taro/types/input_type.rb +13 -0
  39. data/lib/taro/types/list_type.rb +30 -0
  40. data/lib/taro/types/object_type.rb +19 -0
  41. data/lib/taro/types/object_types/free_form_type.rb +13 -0
  42. data/lib/taro/types/object_types/no_content_type.rb +16 -0
  43. data/lib/taro/types/object_types/page_info_type.rb +6 -0
  44. data/lib/taro/types/object_types/page_type.rb +45 -0
  45. data/lib/taro/types/scalar/boolean_type.rb +19 -0
  46. data/lib/taro/types/scalar/float_type.rb +15 -0
  47. data/lib/taro/types/scalar/integer_type.rb +11 -0
  48. data/lib/taro/types/scalar/iso8601_date_type.rb +23 -0
  49. data/lib/taro/types/scalar/iso8601_datetime_type.rb +25 -0
  50. data/lib/taro/types/scalar/string_type.rb +15 -0
  51. data/lib/taro/types/scalar/timestamp_type.rb +23 -0
  52. data/lib/taro/types/scalar/uuid_v4_type.rb +22 -0
  53. data/lib/taro/types/scalar_type.rb +7 -0
  54. data/lib/taro/types/shared/additional_properties.rb +12 -0
  55. data/lib/taro/types/shared/custom_field_resolvers.rb +33 -0
  56. data/lib/taro/types/shared/derivable_types.rb +9 -0
  57. data/lib/taro/types/shared/description.rb +9 -0
  58. data/lib/taro/types/shared/errors.rb +13 -0
  59. data/lib/taro/types/shared/fields.rb +57 -0
  60. data/lib/taro/types/shared/item_type.rb +16 -0
  61. data/lib/taro/types/shared/object_coercion.rb +16 -0
  62. data/lib/taro/types/shared/openapi_name.rb +30 -0
  63. data/lib/taro/types/shared/openapi_type.rb +27 -0
  64. data/lib/taro/types/shared/rendering.rb +22 -0
  65. data/lib/taro/types/shared.rb +3 -0
  66. data/lib/taro/types.rb +3 -0
  67. data/lib/taro/version.rb +2 -3
  68. data/lib/taro.rb +1 -6
  69. data/tasks/benchmark.rake +40 -0
  70. data/tasks/benchmark_1kb.json +23 -0
  71. metadata +91 -7
@@ -0,0 +1,78 @@
1
+ require_relative 'field_validation'
2
+
3
+ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc) do
4
+ include Taro::Types::FieldValidation
5
+
6
+ def initialize(name:, type:, null:, method: name, default: :none, enum: nil, defined_at: nil, desc: nil)
7
+ enum = coerce_to_enum(enum)
8
+ super(name:, type:, null:, method:, default:, enum:, defined_at:, desc:)
9
+ end
10
+
11
+ def value_for_input(object)
12
+ value = object[name] if object
13
+ value = coerce_value(value, true)
14
+ validated_value(value)
15
+ end
16
+
17
+ def value_for_response(object, context: nil, object_is_hash: true)
18
+ value = retrieve_response_value(object, context, object_is_hash)
19
+ value = coerce_value(value, false)
20
+ validated_value(value, false)
21
+ end
22
+
23
+ def default_specified?
24
+ !default.equal?(:none)
25
+ end
26
+
27
+ def openapi_type
28
+ type.openapi_type
29
+ end
30
+
31
+ private
32
+
33
+ def coerce_to_enum(arg)
34
+ return if arg.nil?
35
+
36
+ enum = arg.to_a
37
+ test = Class.new(Taro::Types::EnumType) { arg.each { |v| value(v) } }
38
+ test.raise_if_empty_enum
39
+ enum
40
+ end
41
+
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)
46
+ context.public_send(method)
47
+ elsif object.respond_to?(method, true)
48
+ object.public_send(method)
49
+ else
50
+ raise_response_coercion_error(object)
51
+ end
52
+ end
53
+
54
+ def retrieve_hash_value(object)
55
+ if object.key?(method.to_s)
56
+ object[method.to_s]
57
+ else
58
+ object[method]
59
+ end
60
+ end
61
+
62
+ def coerce_value(value, from_input)
63
+ return if value.nil? && null
64
+ return default if value.nil? && default_specified?
65
+
66
+ type_obj = type.new(value)
67
+ 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
+ end
78
+ end
@@ -0,0 +1,27 @@
1
+ module Taro::Types::FieldValidation
2
+ # Validate the value against the field properties. This method will raise
3
+ # a Taro::InputError or Taro::ResponseError if the value is not matching.
4
+ def validated_value(value, for_input = true)
5
+ validate_null_and_ok?(value, for_input)
6
+ validate_enum_inclusion(value, for_input)
7
+ value
8
+ end
9
+
10
+ private
11
+
12
+ def validate_null_and_ok?(value, for_input)
13
+ return if null || !value.nil?
14
+
15
+ raise for_input ? Taro::InputError : Taro::ResponseError, <<~MSG
16
+ Field #{name} is not nullable (got #{value.inspect})
17
+ MSG
18
+ end
19
+
20
+ def validate_enum_inclusion(value, for_input)
21
+ return if enum.nil? || enum.include?(value)
22
+
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
26
+ end
27
+ end
@@ -0,0 +1,13 @@
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
9
+
10
+ def coerce_response
11
+ response_error 'InputTypes cannot be used as response types'
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # Abstract base class for List types (arrays in OpenAPI terms).
2
+ # Unlike other types, this one should not be manually inherited from,
3
+ # but is used indirectly via `array_of: SomeType`.
4
+ class Taro::Types::ListType < Taro::Types::BaseType
5
+ extend Taro::Types::Shared::DerivableType
6
+ extend Taro::Types::Shared::ItemType
7
+
8
+ self.openapi_type = :array
9
+
10
+ def coerce_input
11
+ object.instance_of?(Array) || input_error('must be an Array')
12
+
13
+ item_type = self.class.item_type
14
+ object.map { |el| item_type.new(el).coerce_input }
15
+ end
16
+
17
+ def coerce_response
18
+ object.respond_to?(:map) || response_error('must be an Enumerable')
19
+
20
+ item_type = self.class.item_type
21
+ object.map { |el| item_type.new(el).coerce_response }
22
+ end
23
+ end
24
+
25
+ # add shortcut to other types
26
+ class Taro::Types::BaseType
27
+ def self.array
28
+ Taro::Types::ListType.for(self)
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # Abstract base class for renderable types with fields.
2
+ class Taro::Types::ObjectType < 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
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
+ end
16
+
17
+ module Taro::Types::ObjectTypes
18
+ Dir[File.join(__dir__, 'object_types', '**', '*.rb')].each { |f| require f }
19
+ end
@@ -0,0 +1,13 @@
1
+ class Taro::Types::ObjectTypes::FreeFormType < Taro::Types::ObjectType
2
+ self.desc = 'An arbitrary, unvalidated Hash or JSON object. Use with care.'
3
+ self.additional_properties = true
4
+
5
+ def coerce_input
6
+ object.is_a?(Hash) && object || input_error('must be a Hash')
7
+ end
8
+
9
+ def coerce_response
10
+ object.respond_to?(:as_json) && (res = object.as_json).is_a?(Hash) && res ||
11
+ response_error('must return a Hash from #as_json')
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ class Taro::Types::ObjectTypes::NoContentType < Taro::Types::ObjectType
2
+ self.desc = 'An empty response'
3
+
4
+ # render takes no arguments in this case
5
+ def self.render
6
+ super(nil)
7
+ end
8
+
9
+ def coerce_input
10
+ input_error 'NoContentType cannot be used as input type'
11
+ end
12
+
13
+ def coerce_response
14
+ {}
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ class Taro::Types::ObjectTypes::PageInfoType < Taro::Types::ObjectType
2
+ field :has_previous_page, type: 'Boolean', null: false, desc: 'Whether there is a previous page of results'
3
+ field :has_next_page, type: 'Boolean', null: false, desc: 'Whether there is another page of results'
4
+ field :start_cursor, type: 'String', null: true, desc: 'The first cursor in the current page of results (null if zero results)'
5
+ field :end_cursor, type: 'String', null: true, desc: 'The last cursor in the current page of results (null if zero results)'
6
+ end
@@ -0,0 +1,45 @@
1
+ # Abstract base class for Page types (paginated ActiveRecord data).
2
+ # Unlike other types, this one should not be manually inherited from,
3
+ # but is used indirectly via `page_of: SomeType`.
4
+ #
5
+ # The gem rails_cursor_pagination must be installed to use this.
6
+ #
7
+ class Taro::Types::ObjectTypes::PageType < Taro::Types::BaseType
8
+ extend Taro::Types::Shared::DerivableType
9
+ extend Taro::Types::Shared::ItemType
10
+
11
+ def coerce_input
12
+ input_error 'PageTypes cannot be used as input types'
13
+ end
14
+
15
+ def coerce_response(after:, limit: 20, order_by: nil, order: nil)
16
+ list = RailsCursorPagination::Paginator.new(
17
+ object, limit:, order_by:, order:, after:
18
+ ).fetch
19
+ coerce_paginated_list(list)
20
+ end
21
+
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
27
+
28
+ {
29
+ self.class.items_key => items,
30
+ page_info: Taro::Types::ObjectTypes::PageInfoType.new(list[:page_info]).coerce_response,
31
+ }
32
+ end
33
+
34
+ # support overrides, e.g. based on item_type
35
+ def self.items_key
36
+ :page
37
+ end
38
+ end
39
+
40
+ # add shortcut to other types
41
+ class Taro::Types::BaseType
42
+ def self.page
43
+ Taro::Types::ObjectTypes::PageType.for(self)
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ class Taro::Types::Scalar::BooleanType < Taro::Types::ScalarType
2
+ self.openapi_type = :boolean
3
+
4
+ def coerce_input
5
+ if object == true || object == false
6
+ object
7
+ else
8
+ input_error('must be true or false')
9
+ end
10
+ end
11
+
12
+ def coerce_response
13
+ if object == true || object == false
14
+ object
15
+ else
16
+ response_error('must be true or false')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ class Taro::Types::Scalar::FloatType < Taro::Types::ScalarType
2
+ self.openapi_type = :number
3
+
4
+ def coerce_input
5
+ object.instance_of?(Float) ? object : input_error('must be a Float')
6
+ end
7
+
8
+ def coerce_response
9
+ case object
10
+ when Float then object
11
+ when Integer then object.to_f
12
+ else response_error('must be a Float or Integer')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ class Taro::Types::Scalar::IntegerType < Taro::Types::ScalarType
2
+ self.openapi_type = :integer
3
+
4
+ def coerce_input
5
+ object.instance_of?(Integer) ? object : input_error('must be an Integer')
6
+ end
7
+
8
+ def coerce_response
9
+ object.instance_of?(Integer) ? object : response_error('must be an Integer')
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ class Taro::Types::Scalar::ISO8601DateType < Taro::Types::ScalarType
2
+ self.desc = 'Represents a time as Date in ISO8601 format.'
3
+ self.openapi_type = :string
4
+
5
+ PATTERN = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\z/
6
+
7
+ def coerce_input
8
+ if object.instance_of?(String) && object.match?(PATTERN)
9
+ Date.parse(object)
10
+ else
11
+ input_error("must be a ISO8601 formatted string")
12
+ end
13
+ end
14
+
15
+ def coerce_response
16
+ case object
17
+ when Date, DateTime, Time
18
+ object.strftime("%Y-%m-%d")
19
+ else
20
+ response_error("must be a Time, Date, or DateTime")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ class Taro::Types::Scalar::ISO8601DateTimeType < Taro::Types::ScalarType
2
+ self.desc = 'Represents a time as DateTime in ISO8601 format.'
3
+ self.openapi_type = :string
4
+
5
+ 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/
6
+
7
+ def coerce_input
8
+ if object.instance_of?(String) && object.match?(PATTERN)
9
+ DateTime.iso8601(object)
10
+ else
11
+ input_error("must be a ISO8601 formatted string")
12
+ end
13
+ end
14
+
15
+ def coerce_response
16
+ case object
17
+ when Date
18
+ object.to_datetime.utc.iso8601
19
+ when DateTime, Time
20
+ object.utc.iso8601
21
+ else
22
+ response_error("must be a Time, Date, or DateTime")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ class Taro::Types::Scalar::StringType < Taro::Types::ScalarType
2
+ self.openapi_type = :string
3
+
4
+ def coerce_input
5
+ object.instance_of?(String) ? object : input_error('must be a String')
6
+ end
7
+
8
+ def coerce_response
9
+ case object
10
+ when String then object
11
+ when Symbol then object.to_s
12
+ else response_error('must be a String or Symbol')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ class Taro::Types::Scalar::TimestampType < Taro::Types::ScalarType
2
+ self.desc = 'Represents a time as Time on the server side and UNIX timestamp (integer) on the client side.'
3
+ self.openapi_type = :integer
4
+
5
+ def coerce_input
6
+ if object.instance_of?(Integer)
7
+ Time.at(object)
8
+ else
9
+ input_error("must be an Integer")
10
+ end
11
+ end
12
+
13
+ def coerce_response
14
+ case object
15
+ when Date, DateTime, Time
16
+ object.strftime('%s').to_i
17
+ when Integer
18
+ object
19
+ else
20
+ response_error("must be a Time, Date, DateTime, or Integer")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ class Taro::Types::Scalar::UUIDv4Type < Taro::Types::ScalarType
2
+ self.desc = "A UUID v4 string"
3
+ self.openapi_type = :string
4
+
5
+ PATTERN = /\A\h{8}-?(?:\h{4}-?){3}\h{12}\z/
6
+
7
+ def coerce_input
8
+ if object.is_a?(String) && object.match?(PATTERN)
9
+ object
10
+ else
11
+ input_error("must be a UUID v4 string")
12
+ end
13
+ end
14
+
15
+ def coerce_response
16
+ if object.is_a?(String) && object.match?(PATTERN)
17
+ object
18
+ else
19
+ response_error("must be a UUID v4 string")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # Abstract base class for scalar types, i.e. types without fields.
2
+ class Taro::Types::ScalarType < Taro::Types::BaseType
3
+ end
4
+
5
+ module Taro::Types::Scalar
6
+ Dir[File.join(__dir__, 'scalar', '**', '*.rb')].each { |f| require f }
7
+ end
@@ -0,0 +1,12 @@
1
+ module Taro::Types::Shared::AdditionalProperties
2
+ attr_writer :additional_properties
3
+
4
+ def additional_properties?
5
+ !!@additional_properties
6
+ end
7
+
8
+ def inherited(subclass)
9
+ super
10
+ subclass.additional_properties = @additional_properties
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ # Allows implementing methods on types to override or implement the method
2
+ # used to retrieve the value of a field.
3
+ module Taro::Types::Shared::CustomFieldResolvers
4
+ def resolve?(method)
5
+ self.class.custom_resolvers.key?(method)
6
+ end
7
+
8
+ def self.included(mod)
9
+ mod.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def custom_resolvers
14
+ @custom_resolvers ||= {}
15
+ end
16
+
17
+ def method_added(name)
18
+ if name == :object
19
+ raise(Taro::ArgumentError, '#object is a reserved, internally used method name')
20
+ elsif ![:coerce_input, :coerce_response].include?(name) &&
21
+ !self.name.to_s.start_with?('Taro::Types::')
22
+ custom_resolvers[name] = true
23
+ end
24
+
25
+ super
26
+ end
27
+
28
+ def inherited(subclass)
29
+ subclass.instance_variable_set(:@custom_resolvers, custom_resolvers.dup)
30
+ super
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Taro::Types::Shared::DerivableType
2
+ def for(type)
3
+ derived_types[type] ||= Class.new(self).tap { |t| t.item_type = type }
4
+ end
5
+
6
+ def derived_types
7
+ @derived_types ||= {}
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Taro::Types::Shared::Description
2
+ def desc
3
+ @desc
4
+ end
5
+
6
+ def desc=(arg)
7
+ @desc = arg
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Taro::Types::Shared::Errors
2
+ def input_error(msg)
3
+ raise Taro::InputError, coerce_error_message(msg)
4
+ end
5
+
6
+ def response_error(msg)
7
+ raise Taro::ResponseError, coerce_error_message(msg)
8
+ end
9
+
10
+ def coerce_error_message(msg)
11
+ "#{object.inspect} (#{object.class}) is not valid as #{self.class}: #{msg}"
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ # Adds the `::field` method to object and input types.
2
+ module Taro::Types::Shared::Fields
3
+ # Field types are set using class name Strings. The respective type classes
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.
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)
11
+
12
+ field_defs[name] = { name:, defined_at:, **kwargs }
13
+ end
14
+
15
+ def fields
16
+ @fields ||= evaluate_field_defs
17
+ end
18
+
19
+ private
20
+
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
+ (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}")
32
+
33
+ kwargs[type_keys.first].class == String ||
34
+ raise(Taro::ArgumentError, "#{type_key} must be a String for field #{name} at #{defined_at}")
35
+ end
36
+
37
+ def validate_no_override(name, defined_at:)
38
+ prev = field_defs[name]
39
+ prev && raise(Taro::ArgumentError, "field #{name} at #{defined_at} previously defined at #{prev[:defined_at]}")
40
+ end
41
+
42
+ def field_defs
43
+ @field_defs ||= {}
44
+ end
45
+
46
+ def evaluate_field_defs
47
+ field_defs.transform_values do |field_def|
48
+ type = Taro::Types::Coercion.call(field_def)
49
+ Taro::Types::Field.new(**field_def.except(*Taro::Types::Coercion::KEYS), type:)
50
+ end
51
+ end
52
+
53
+ def inherited(subclass)
54
+ subclass.instance_variable_set(:@field_defs, field_defs.dup)
55
+ super
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ # Provides coercion and validation for the "inner" type of enums and arrays.
2
+ module Taro::Types::Shared::ItemType
3
+ attr_reader :item_type
4
+
5
+ def item_type=(new_type)
6
+ item_type.nil? || new_type == item_type || raise_mixed_types(new_type)
7
+ @item_type = new_type
8
+ end
9
+
10
+ def raise_mixed_types(new_type)
11
+ raise Taro::ArgumentError, <<~MSG
12
+ All items must be of the same type. Mixed types are not supported for now.
13
+ Expected another #{item_type} item but got a #{new_type} for #{self}.
14
+ MSG
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # Provides input and response handling for types with fields.
2
+ module Taro::Types::Shared::ObjectCoercion
3
+ def coerce_input
4
+ self.class.fields.transform_values do |field|
5
+ field.value_for_input(object)
6
+ end
7
+ end
8
+
9
+ # Render the object into a hash.
10
+ def coerce_response
11
+ object_is_hash = object.is_a?(Hash)
12
+ self.class.fields.transform_values do |field|
13
+ field.value_for_response(object, context: self, object_is_hash:)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # Provides a setter, getter and defaults for type classes' `openapi_name`,
2
+ # for use in the OpenAPI export ($refs and corresponding component names).
3
+ module Taro::Types::Shared::OpenAPIName
4
+ def openapi_name
5
+ @openapi_name ||= default_openapi_name
6
+ end
7
+
8
+ def openapi_name=(arg)
9
+ arg.nil? || arg.is_a?(String) ||
10
+ raise(Taro::ArgumentError, 'openapi_name must be a String')
11
+ @openapi_name = arg
12
+ end
13
+
14
+ def default_openapi_name # rubocop:disable Metrics
15
+ if self < Taro::Types::EnumType ||
16
+ self < Taro::Types::InputType ||
17
+ self < Taro::Types::ObjectType
18
+ name && name.chomp('Type').gsub('::', '_') ||
19
+ raise(Taro::Error, 'openapi_name must be set for anonymous type classes')
20
+ elsif self < Taro::Types::ScalarType
21
+ 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
+ else
27
+ raise NotImplementedError, 'no default_openapi_name for this type'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # Provides a setter and getter for type classes' `openapi_type`,
2
+ # for use in the OpenAPI export.
3
+ module Taro::Types::Shared::OpenAPIType
4
+ OPENAPI_TYPES = %i[
5
+ array
6
+ boolean
7
+ integer
8
+ number
9
+ object
10
+ string
11
+ ].freeze
12
+
13
+ def openapi_type
14
+ @openapi_type || raise(Taro::RuntimeError, "Type lacks openapi_type: #{self}")
15
+ end
16
+
17
+ def openapi_type=(arg)
18
+ OPENAPI_TYPES.include?(arg) ||
19
+ raise(Taro::ArgumentError, "openapi_type must be a Symbol, one of #{OPENAPI_TYPES}")
20
+ @openapi_type = arg
21
+ end
22
+
23
+ def inherited(subclass)
24
+ subclass.instance_variable_set(:@openapi_type, @openapi_type)
25
+ super
26
+ end
27
+ end