taro 0.0.0 → 1.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 (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