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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/CHANGELOG.md +10 -0
- data/README.md +257 -1
- data/Rakefile +11 -0
- data/lib/taro/config.rb +22 -0
- data/lib/taro/errors.rb +12 -0
- data/lib/taro/export/base.rb +29 -0
- data/lib/taro/export/open_api_v3.rb +190 -0
- data/lib/taro/export.rb +3 -0
- data/lib/taro/rails/active_declarations.rb +19 -0
- data/lib/taro/rails/declaration.rb +118 -0
- data/lib/taro/rails/declaration_buffer.rb +24 -0
- data/lib/taro/rails/dsl.rb +18 -0
- data/lib/taro/rails/generators/install_generator.rb +19 -0
- data/lib/taro/rails/generators/templates/enum_type.erb +4 -0
- data/lib/taro/rails/generators/templates/error_type.erb +10 -0
- data/lib/taro/rails/generators/templates/errors_type.erb +25 -0
- data/lib/taro/rails/generators/templates/input_type.erb +4 -0
- data/lib/taro/rails/generators/templates/no_content_type.erb +4 -0
- data/lib/taro/rails/generators/templates/object_type.erb +4 -0
- data/lib/taro/rails/generators/templates/scalar_type.erb +4 -0
- data/lib/taro/rails/generators.rb +3 -0
- data/lib/taro/rails/normalized_route.rb +29 -0
- data/lib/taro/rails/param_parsing.rb +19 -0
- data/lib/taro/rails/railtie.rb +15 -0
- data/lib/taro/rails/response_validation.rb +13 -0
- data/lib/taro/rails/response_validator.rb +109 -0
- data/lib/taro/rails/route_finder.rb +35 -0
- data/lib/taro/rails/tasks/export.rake +15 -0
- data/lib/taro/rails.rb +17 -0
- data/lib/taro/types/base_type.rb +17 -0
- data/lib/taro/types/coercion.rb +73 -0
- data/lib/taro/types/enum_type.rb +43 -0
- data/lib/taro/types/field.rb +78 -0
- data/lib/taro/types/field_validation.rb +27 -0
- data/lib/taro/types/input_type.rb +13 -0
- data/lib/taro/types/list_type.rb +30 -0
- data/lib/taro/types/object_type.rb +19 -0
- data/lib/taro/types/object_types/free_form_type.rb +13 -0
- data/lib/taro/types/object_types/no_content_type.rb +16 -0
- data/lib/taro/types/object_types/page_info_type.rb +6 -0
- data/lib/taro/types/object_types/page_type.rb +45 -0
- data/lib/taro/types/scalar/boolean_type.rb +19 -0
- data/lib/taro/types/scalar/float_type.rb +15 -0
- data/lib/taro/types/scalar/integer_type.rb +11 -0
- data/lib/taro/types/scalar/iso8601_date_type.rb +23 -0
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +25 -0
- data/lib/taro/types/scalar/string_type.rb +15 -0
- data/lib/taro/types/scalar/timestamp_type.rb +23 -0
- data/lib/taro/types/scalar/uuid_v4_type.rb +22 -0
- data/lib/taro/types/scalar_type.rb +7 -0
- data/lib/taro/types/shared/additional_properties.rb +12 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +33 -0
- data/lib/taro/types/shared/derivable_types.rb +9 -0
- data/lib/taro/types/shared/description.rb +9 -0
- data/lib/taro/types/shared/errors.rb +13 -0
- data/lib/taro/types/shared/fields.rb +57 -0
- data/lib/taro/types/shared/item_type.rb +16 -0
- data/lib/taro/types/shared/object_coercion.rb +16 -0
- data/lib/taro/types/shared/openapi_name.rb +30 -0
- data/lib/taro/types/shared/openapi_type.rb +27 -0
- data/lib/taro/types/shared/rendering.rb +22 -0
- data/lib/taro/types/shared.rb +3 -0
- data/lib/taro/types.rb +3 -0
- data/lib/taro/version.rb +2 -3
- data/lib/taro.rb +1 -6
- data/tasks/benchmark.rake +40 -0
- data/tasks/benchmark_1kb.json +23 -0
- 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,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,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
|