taro 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/CHANGELOG.md +5 -0
- data/README.md +262 -1
- data/Rakefile +11 -0
- data/lib/taro/config.rb +22 -0
- data/lib/taro/errors.rb +6 -0
- data/lib/taro/export/base.rb +29 -0
- data/lib/taro/export/open_api_v3.rb +189 -0
- data/lib/taro/export.rb +3 -0
- data/lib/taro/rails/active_declarations.rb +19 -0
- data/lib/taro/rails/declaration.rb +101 -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 +63 -0
- data/lib/taro/rails/route_finder.rb +35 -0
- data/lib/taro/rails/tasks/export.rake +15 -0
- data/lib/taro/rails.rb +18 -0
- data/lib/taro/types/base_type.rb +17 -0
- data/lib/taro/types/coercion.rb +72 -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 +36 -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 +90 -7
@@ -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
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# The `::render` method is intended for use in controllers.
|
2
|
+
# Special types (e.g. PageType) may accept kwargs for `#coerce_response`.
|
3
|
+
module Taro::Types::Shared::Rendering
|
4
|
+
def render(object, opts = {})
|
5
|
+
if (prev = rendering)
|
6
|
+
raise Taro::RuntimeError, <<~MSG
|
7
|
+
Type.render should only be called once per request.
|
8
|
+
(First called on #{prev}, then on #{self}.)
|
9
|
+
MSG
|
10
|
+
end
|
11
|
+
|
12
|
+
result = new(object).coerce_response(**opts)
|
13
|
+
|
14
|
+
# Only mark this as the used type if coercion worked so that
|
15
|
+
# rescue_from can be used to render another type.
|
16
|
+
self.rendering = self
|
17
|
+
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def rendering=(value)
|
22
|
+
ActiveSupport::IsolatedExecutionState[:taro_type_rendering] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def rendering
|
26
|
+
ActiveSupport::IsolatedExecutionState[:taro_type_rendering]
|
27
|
+
end
|
28
|
+
|
29
|
+
def used_in_response=(value)
|
30
|
+
ActiveSupport::IsolatedExecutionState[:taro_type_used_in_response] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def used_in_response
|
34
|
+
ActiveSupport::IsolatedExecutionState[:taro_type_used_in_response]
|
35
|
+
end
|
36
|
+
end
|
data/lib/taro/types.rb
ADDED
data/lib/taro/version.rb
CHANGED
data/lib/taro.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
task :benchmark do
|
2
|
+
require 'benchmark/ips'
|
3
|
+
require 'json'
|
4
|
+
require_relative '../lib/taro'
|
5
|
+
|
6
|
+
data = JSON.load_file("#{__dir__}/benchmark_1kb.json", symbolize_names: true)
|
7
|
+
|
8
|
+
item_type = Class.new(Taro::Types::ObjectType) do
|
9
|
+
field :name, type: 'String', null: false
|
10
|
+
field :language, type: 'String', null: false
|
11
|
+
field :id, type: 'String', null: false
|
12
|
+
field :bio, type: 'String', null: false
|
13
|
+
field :version, type: 'Float', null: false
|
14
|
+
end
|
15
|
+
|
16
|
+
type = Taro::Types::ListType.for(item_type)
|
17
|
+
|
18
|
+
# 143.889k (± 2.7%) i/s - 723.816k in 5.034247s
|
19
|
+
Benchmark.ips do |x|
|
20
|
+
x.report('parse 1 KB of params') { type.new(data).coerce_input }
|
21
|
+
end
|
22
|
+
|
23
|
+
# 103.382k (± 6.5%) i/s - 522.550k in 5.087725s
|
24
|
+
Benchmark.ips do |x|
|
25
|
+
x.report('validate a 1 KB response') { type.new(data).coerce_response }
|
26
|
+
end
|
27
|
+
|
28
|
+
big_data = data * 1000
|
29
|
+
big_data.each { |el| el.merge('version' => rand) }
|
30
|
+
|
31
|
+
# 101.359 (± 5.9%) i/s - 513.000 in 5.078335s
|
32
|
+
Benchmark.ips do |x|
|
33
|
+
x.report('parse 1 MB of params') { type.new(big_data).coerce_input }
|
34
|
+
end
|
35
|
+
|
36
|
+
# 84.412 (± 2.4%) i/s - 427.000 in 5.061117s
|
37
|
+
Benchmark.ips do |x|
|
38
|
+
x.report('validate a 1 MB response') { type.new(big_data).coerce_response }
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"name": "Adeel Solangi",
|
4
|
+
"language": "Sindhi",
|
5
|
+
"id": "V59OF92YF627HFY0",
|
6
|
+
"bio": "Donec lobortis eleifend condimentum. Cras dictum dolor lacinia lectus vehicula rutrum. Maecenas quis nisi nunc. Nam tristique feugiat est vitae mollis. Maecenas quis nisi nunc.",
|
7
|
+
"version": 6.1
|
8
|
+
},
|
9
|
+
{
|
10
|
+
"name": "Afzal Ghaffar",
|
11
|
+
"language": "Sindhi",
|
12
|
+
"id": "ENTOCR13RSCLZ6KU",
|
13
|
+
"bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Etiam congue dignissim volutpat. Vestibulum pharetra libero et velit gravida euismod.",
|
14
|
+
"version": 1.88
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"name": "Aamir Solangi",
|
18
|
+
"language": "Sindhi",
|
19
|
+
"id": "IAKPO3R4761JDRVG",
|
20
|
+
"bio": "Vestibulum pharetra libero et velit gravida euismod. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Fusce eu ultrices elit, vel posuere neque.",
|
21
|
+
"version": 7.27
|
22
|
+
}
|
23
|
+
]
|