taro 0.0.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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