taro 0.0.0 → 1.0.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/CHANGELOG.md +5 -0
  5. data/README.md +262 -1
  6. data/Rakefile +11 -0
  7. data/lib/taro/config.rb +22 -0
  8. data/lib/taro/errors.rb +6 -0
  9. data/lib/taro/export/base.rb +29 -0
  10. data/lib/taro/export/open_api_v3.rb +189 -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 +101 -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 +63 -0
  29. data/lib/taro/rails/route_finder.rb +35 -0
  30. data/lib/taro/rails/tasks/export.rake +15 -0
  31. data/lib/taro/rails.rb +18 -0
  32. data/lib/taro/types/base_type.rb +17 -0
  33. data/lib/taro/types/coercion.rb +72 -0
  34. data/lib/taro/types/enum_type.rb +43 -0
  35. data/lib/taro/types/field.rb +78 -0
  36. data/lib/taro/types/field_validation.rb +27 -0
  37. data/lib/taro/types/input_type.rb +13 -0
  38. data/lib/taro/types/list_type.rb +30 -0
  39. data/lib/taro/types/object_type.rb +19 -0
  40. data/lib/taro/types/object_types/free_form_type.rb +13 -0
  41. data/lib/taro/types/object_types/no_content_type.rb +16 -0
  42. data/lib/taro/types/object_types/page_info_type.rb +6 -0
  43. data/lib/taro/types/object_types/page_type.rb +45 -0
  44. data/lib/taro/types/scalar/boolean_type.rb +19 -0
  45. data/lib/taro/types/scalar/float_type.rb +15 -0
  46. data/lib/taro/types/scalar/integer_type.rb +11 -0
  47. data/lib/taro/types/scalar/iso8601_date_type.rb +23 -0
  48. data/lib/taro/types/scalar/iso8601_datetime_type.rb +25 -0
  49. data/lib/taro/types/scalar/string_type.rb +15 -0
  50. data/lib/taro/types/scalar/timestamp_type.rb +23 -0
  51. data/lib/taro/types/scalar/uuid_v4_type.rb +22 -0
  52. data/lib/taro/types/scalar_type.rb +7 -0
  53. data/lib/taro/types/shared/additional_properties.rb +12 -0
  54. data/lib/taro/types/shared/custom_field_resolvers.rb +33 -0
  55. data/lib/taro/types/shared/derivable_types.rb +9 -0
  56. data/lib/taro/types/shared/description.rb +9 -0
  57. data/lib/taro/types/shared/errors.rb +13 -0
  58. data/lib/taro/types/shared/fields.rb +57 -0
  59. data/lib/taro/types/shared/item_type.rb +16 -0
  60. data/lib/taro/types/shared/object_coercion.rb +16 -0
  61. data/lib/taro/types/shared/openapi_name.rb +30 -0
  62. data/lib/taro/types/shared/openapi_type.rb +27 -0
  63. data/lib/taro/types/shared/rendering.rb +36 -0
  64. data/lib/taro/types/shared.rb +3 -0
  65. data/lib/taro/types.rb +3 -0
  66. data/lib/taro/version.rb +2 -3
  67. data/lib/taro.rb +1 -6
  68. data/tasks/benchmark.rake +40 -0
  69. data/tasks/benchmark_1kb.json +23 -0
  70. metadata +90 -7
@@ -0,0 +1,24 @@
1
+ # Buffers api declarations in rails controllers (e.g. `param :foo, ...`)
2
+ # until the next action method is defined (e.g. `def create`).
3
+ module Taro::Rails::DeclarationBuffer
4
+ def buffered_declaration(controller_class)
5
+ buffered_declarations[controller_class] ||= Taro::Rails::Declaration.new
6
+ end
7
+
8
+ def buffered_declarations
9
+ @buffered_declarations ||= {}
10
+ end
11
+
12
+ def apply_buffered_declaration(controller_class, action_name)
13
+ declaration = pop_buffered_declaration(controller_class)
14
+ return unless declaration
15
+
16
+ declaration.finalize(controller_class:, action_name:)
17
+
18
+ Taro::Rails.apply(declaration:, controller_class:, action_name:)
19
+ end
20
+
21
+ def pop_buffered_declaration(controller_class)
22
+ buffered_declarations.delete(controller_class)
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module Taro::Rails::DSL
2
+ def api(summary, **kwargs)
3
+ Taro::Rails.buffered_declaration(self).add_info(summary, **kwargs)
4
+ end
5
+
6
+ def param(param_name, **kwargs)
7
+ Taro::Rails.buffered_declaration(self).add_param(param_name, **kwargs)
8
+ end
9
+
10
+ def returns(field_name = nil, **kwargs)
11
+ Taro::Rails.buffered_declaration(self).add_return(field_name, **kwargs)
12
+ end
13
+
14
+ def method_added(method_name)
15
+ Taro::Rails.apply_buffered_declaration(self, method_name)
16
+ super
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/base'
3
+
4
+ class Taro::Rails::Generators::InstallGenerator < ::Rails::Generators::Base
5
+ desc 'Set up Taro base type files in your Rails app'
6
+
7
+ class_option :dir, type: :string, default: "app/types"
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ # :nocov:
12
+ def create_type_files
13
+ Dir["#{self.class.source_root}/**/*.erb"].each do |tmpl|
14
+ dest_dir = options[:dir].chomp('/')
15
+ copy_file tmpl, "#{dest_dir}/#{File.basename(tmpl).sub('erb', 'rb')}"
16
+ end
17
+ end
18
+ # :nocov:
19
+ end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you don't have to inherit from
2
+ # Taro::Types::EnumType directly. You can customize or delete it.
3
+ class EnumType < Taro::Types::EnumType
4
+ end
@@ -0,0 +1,10 @@
1
+ # This file is generated by taro.
2
+ # This and ErrorsType are a starting point for unified error presentation.
3
+ # You can use them to render errors from various sources (ActiveRecord etc.)
4
+ # and render error responses in rescue_from in a consistent way.
5
+ # You can customize these files to fit your needs, or delete them.
6
+ class ErrorType < ObjectType
7
+ field :attribute, type: 'String', null: true, desc: 'Attribute name'
8
+ field :code, type: 'String', null: false, method: :type, desc: 'Error code'
9
+ field :message, type: 'String', null: true, desc: 'Error message'
10
+ end
@@ -0,0 +1,25 @@
1
+ # This file is generated by taro.
2
+ # This and ErrorType are a starting point for unified error presentation.
3
+ # You can use them to render errors from various sources (ActiveRecord etc.)
4
+ # and render error responses in rescue_from in a consistent way.
5
+ # You can customize these files to fit your needs, or delete them.
6
+ class ErrorsType < Taro::Types::ListType
7
+ self.item_type = ErrorType
8
+
9
+ def coerce_input
10
+ input_error 'ErrorsType cannot be used as input type'
11
+ end
12
+
13
+ def coerce_response
14
+ case object.class.name
15
+ when 'ActiveRecord::Base'
16
+ super(object.errors.errors)
17
+ when 'ActiveModel::Errors'
18
+ super(object.errors)
19
+ when 'Hash', 'Interactor::Context'
20
+ super(object[:errors])
21
+ else # e.g. Array
22
+ super(object)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you don't have to inherit from
2
+ # Taro::Types::InputType directly. You can customize or delete it.
3
+ class InputType < Taro::Types::InputType
4
+ end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you can do `NoContentType.render`
2
+ # without using the deeply nested original type. You can customize or delete it.
3
+ class NoContentType < Taro::Types::ObjectTypes::NoContentType
4
+ end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you don't have to inherit from
2
+ # Taro::Types::ObjectType directly. You can customize or delete it.
3
+ class ObjectType < Taro::Types::ObjectType
4
+ end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you don't have to inherit from
2
+ # Taro::Types::ScalarType directly. You can customize or delete it.
3
+ class ScalarType < Taro::Types::ScalarType
4
+ end
@@ -0,0 +1,3 @@
1
+ module Taro::Rails::Generators
2
+ Dir[File.join(__dir__, 'generators', '**', '*.rb')].each { |f| require f }
3
+ end
@@ -0,0 +1,29 @@
1
+ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
2
+ def ignored?
3
+ verb.to_s.empty? || patch_update?
4
+ end
5
+
6
+ # Journey::Route#verb is a String. Its usually something like 'POST', but
7
+ # manual matched routes may have e.g. 'GET|POST' (🤢). We only need one copy.
8
+ def verb
9
+ rails_route.verb.to_s.scan(/\w+/).sort.last&.downcase
10
+ end
11
+
12
+ # Rails has both PATCH and PUT routes for updates. We only need one copy.
13
+ def patch_update?
14
+ verb == 'patch' && rails_route.requirements[:action] == 'update'
15
+ end
16
+
17
+ def openapi_path
18
+ rails_route.path.spec.to_s.gsub('(.:format)', '').gsub(/:(\w+)/, '{\1}')
19
+ end
20
+
21
+ def path_params
22
+ openapi_path.scan(/{(\w+)}/).flatten.map(&:to_sym)
23
+ end
24
+
25
+ def endpoint
26
+ controller, action = rails_route.requirements.values_at(:controller, :action)
27
+ "#{controller}##{action}"
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module Taro::Rails::ParamParsing
2
+ def self.install(controller_class:, action_name:)
3
+ return unless Taro.config.parse_params
4
+
5
+ key = [controller_class, action_name]
6
+ return if installed[key]
7
+
8
+ installed[key] = true
9
+
10
+ controller_class.before_action(only: action_name) do
11
+ declaration = Taro::Rails.declaration_for(self)
12
+ @api_params = declaration.parse_params(params)
13
+ end
14
+ end
15
+
16
+ def self.installed
17
+ @installed ||= {}
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ class Taro::Rails::Railtie < ::Rails::Railtie
2
+ initializer("taro") do |app|
3
+ # The `:action_controller` hook fires for both ActionController::API
4
+ # and ActionController::Base, executing the block in their context.
5
+ ActiveSupport.on_load(:action_controller) do
6
+ extend Taro::Rails::DSL
7
+ end
8
+
9
+ app.reloader.to_prepare do
10
+ Taro::Rails.reset
11
+ end
12
+ end
13
+
14
+ rake_tasks { Dir["#{__dir__}/tasks/**/*.rake"].each { |f| load f } }
15
+ end
@@ -0,0 +1,63 @@
1
+ module Taro::Rails::ResponseValidation
2
+ def self.install(controller_class:, action_name:)
3
+ return unless Taro.config.validate_response
4
+
5
+ key = [controller_class, action_name]
6
+ return if installed[key]
7
+
8
+ installed[key] = true
9
+
10
+ controller_class.around_action(only: action_name) do |_, block|
11
+ Taro::Types::BaseType.rendering = nil
12
+ block.call
13
+ Taro::Rails::ResponseValidation.call(self)
14
+ ensure
15
+ Taro::Types::BaseType.rendering = nil
16
+ end
17
+ end
18
+
19
+ def self.installed
20
+ @installed ||= {}
21
+ end
22
+
23
+ def self.call(controller)
24
+ declaration = Taro::Rails.declaration_for(controller)
25
+ nesting = declaration.return_nestings[controller.status]
26
+ expected = declaration.returns[controller.status]
27
+ if nesting
28
+ # case: `returns :some_nesting, type: 'SomeType'` (ad-hoc return type)
29
+ check_nesting(controller.response, nesting)
30
+ expected = expected.fields[nesting].type
31
+ end
32
+
33
+ check_expected_type_was_used(controller, expected)
34
+ end
35
+
36
+ def self.check_nesting(response, nesting)
37
+ return unless /json/.match?(response.media_type)
38
+
39
+ first_key = response.body.to_s[/\A{\s*"([^"]+)"/, 1]
40
+ first_key == nesting.to_s || raise(Taro::ResponseError, <<~MSG)
41
+ Expected response to be nested in "#{nesting}" key, but it was not.
42
+ (First JSON key in response: "#{first_key}".)
43
+ MSG
44
+ end
45
+
46
+ def self.check_expected_type_was_used(controller, expected)
47
+ used = Taro::Types::BaseType.rendering
48
+
49
+ if expected.nil?
50
+ raise(Taro::ResponseError, <<~MSG)
51
+ No matching return type declared in #{controller.class}##{controller.action_name}\
52
+ for status #{controller.status}.
53
+ MSG
54
+ end
55
+
56
+ used&.<=(expected) || raise(Taro::ResponseError, <<~MSG)
57
+ Expected #{controller.class}##{controller.action_name} to use #{expected}.render,
58
+ but #{used ? "#{used}.render" : 'no type render method'} was called.
59
+ MSG
60
+
61
+ Taro::Types::BaseType.used_in_response = used # for comparisons in specs
62
+ end
63
+ end
@@ -0,0 +1,35 @@
1
+ module Taro::Rails::RouteFinder
2
+ class << self
3
+ def call(controller_class:, action_name:)
4
+ endpoint = "#{controller_class.controller_path}##{action_name}"
5
+ cache[endpoint] || []
6
+ end
7
+
8
+ def clear_cache
9
+ @cache = nil
10
+ end
11
+
12
+ private
13
+
14
+ def cache
15
+ @cache ||= build_cache
16
+ end
17
+
18
+ def build_cache
19
+ # Build a Hash like
20
+ # { 'users#show' } => [#<NormalizedRoute>, #<NormalizedRoute>] }
21
+ rails_routes.each_with_object({}) do |rails_route, hash|
22
+ route = Taro::Rails::NormalizedRoute.new(rails_route:)
23
+ next if route.ignored?
24
+
25
+ (hash[route.endpoint] ||= []) << route
26
+ end
27
+ end
28
+
29
+ def rails_routes
30
+ # make sure routes are loaded
31
+ Rails.application.reload_routes! unless Rails.application.routes.routes.any?
32
+ Rails.application.routes.routes
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ desc 'Export all taro API declarations to a file'
2
+ task 'taro:export' => :environment do
3
+ # make sure all declarations have been seen
4
+ Rails.application.eager_load!
5
+
6
+ # the generator / openapi version might become a config option later
7
+ export = Taro::Export::OpenAPIv3.call(
8
+ declarations: Taro::Rails.declarations,
9
+ title: Taro.config.api_name,
10
+ version: Taro.config.api_version,
11
+ )
12
+
13
+ data = export.result.send("to_#{Taro.config.export_format}")
14
+ File.write(Taro.config.export_path, data)
15
+ end
data/lib/taro/rails.rb ADDED
@@ -0,0 +1,18 @@
1
+ # :nocov:
2
+ return unless defined?(::Rails)
3
+ # :nocov:
4
+
5
+ module Taro::Rails
6
+ Dir[File.join(__dir__, "rails", "*.rb")].each { |f| require f }
7
+
8
+ extend ActiveDeclarations
9
+ extend DeclarationBuffer
10
+
11
+ def self.reset
12
+ buffered_declarations.clear
13
+ declarations_map.clear
14
+ RouteFinder.clear_cache
15
+ Taro::Types::BaseType.rendering = nil
16
+ Taro::Types::BaseType.used_in_response = nil
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # Abstract base class for all types.
2
+ #
3
+ # Concrete type classes must set `self.openapi_type` and implement
4
+ # the `#coerce_input` and `#coerce_response` methods.
5
+ #
6
+ # Instances of types are initialized with the object that they represent.
7
+ # The object is a parameter hash for inputs and a manually passed hash
8
+ # or object when rendering a response.
9
+ Taro::Types::BaseType = Data.define(:object) do
10
+ require_relative "shared"
11
+ extend Taro::Types::Shared::AdditionalProperties
12
+ extend Taro::Types::Shared::Description
13
+ extend Taro::Types::Shared::OpenAPIName
14
+ extend Taro::Types::Shared::OpenAPIType
15
+ extend Taro::Types::Shared::Rendering
16
+ include Taro::Types::Shared::Errors
17
+ end
@@ -0,0 +1,72 @@
1
+ module Taro::Types::Coercion
2
+ KEYS = %i[type array_of page_of].freeze
3
+
4
+ class << self
5
+ def call(arg)
6
+ validate_hash(arg)
7
+ from_hash(arg)
8
+ end
9
+
10
+ private
11
+
12
+ def validate_hash(arg)
13
+ arg.is_a?(Hash) || raise(Taro::ArgumentError, <<~MSG)
14
+ Type coercion argument must be a Hash, got: #{arg.inspect} (#{arg.class})
15
+ MSG
16
+
17
+ types = arg.slice(*KEYS)
18
+ types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
19
+ Exactly one of type, array_of, or page_of must be given, got: #{types}
20
+ MSG
21
+ end
22
+
23
+ def from_hash(hash)
24
+ if hash[:type]
25
+ from_string(hash[:type])
26
+ elsif (inner_type = hash[:array_of])
27
+ from_string(inner_type).array
28
+ elsif (inner_type = hash[:page_of])
29
+ from_string(inner_type).page
30
+ else
31
+ raise NotImplementedError, 'Unsupported type coercion'
32
+ end
33
+ end
34
+
35
+ def from_string(arg)
36
+ shortcuts[arg] || from_class(Object.const_get(arg.to_s))
37
+ rescue NameError
38
+ raise Taro::ArgumentError, <<~MSG
39
+ Unsupported type: #{arg}. It should be a type-class name
40
+ or one of #{shortcuts.keys.map(&:inspect).join(', ')}.
41
+ MSG
42
+ end
43
+
44
+ def from_class(arg)
45
+ arg < Taro::Types::BaseType || raise(Taro::ArgumentError, <<~MSG)
46
+ Unsupported type: #{arg}. It should be a subclass of Taro::Types::BaseType.
47
+ MSG
48
+
49
+ arg
50
+ end
51
+
52
+ # Map some Ruby class names and other shortcuts to built-in types
53
+ # to support e.g. `returns 'String'`, or `field :foo, type: 'Boolean'` etc.
54
+ require 'date'
55
+ def shortcuts
56
+ @shortcuts ||= {
57
+ # rubocop:disable Layout/HashAlignment - buggy cop
58
+ 'Boolean' => Taro::Types::Scalar::BooleanType,
59
+ 'Float' => Taro::Types::Scalar::FloatType,
60
+ 'FreeForm' => Taro::Types::ObjectTypes::FreeFormType,
61
+ 'Integer' => Taro::Types::Scalar::IntegerType,
62
+ 'String' => Taro::Types::Scalar::StringType,
63
+ 'Timestamp' => Taro::Types::Scalar::TimestampType,
64
+ 'UUID' => Taro::Types::Scalar::UUIDv4Type,
65
+ 'Date' => Taro::Types::Scalar::ISO8601DateType,
66
+ 'Time' => Taro::Types::Scalar::ISO8601DateTimeType,
67
+ 'DateTime' => Taro::Types::Scalar::ISO8601DateTimeType,
68
+ # rubocop:enable Layout/HashAlignment - buggy cop
69
+ }.freeze
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ # Abstract class.
2
+ class Taro::Types::EnumType < Taro::Types::BaseType
3
+ extend Taro::Types::Shared::ItemType
4
+
5
+ def self.value(value)
6
+ self.item_type = Taro::Types::Coercion.call(type: value.class.name)
7
+ @openapi_type ||= item_type.openapi_type
8
+ values << value
9
+ end
10
+
11
+ def self.values
12
+ @values ||= []
13
+ end
14
+
15
+ def coerce_input
16
+ self.class.raise_if_empty_enum
17
+ value = self.class.item_type.new(object).coerce_input
18
+ if self.class.values.include?(value)
19
+ value
20
+ else
21
+ input_error("must be one of #{self.class.values}")
22
+ end
23
+ end
24
+
25
+ def coerce_response
26
+ self.class.raise_if_empty_enum
27
+ value = self.class.item_type.new(object).coerce_response
28
+ if self.class.values.include?(value)
29
+ value
30
+ else
31
+ response_error("must be one of #{self.class.values}")
32
+ end
33
+ end
34
+
35
+ def self.raise_if_empty_enum
36
+ values.empty? && raise(Taro::RuntimeError, "Enum #{self} has no values")
37
+ end
38
+
39
+ def self.inherited(subclass)
40
+ subclass.instance_variable_set(:@values, values.dup)
41
+ super
42
+ end
43
+ end
@@ -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