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,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,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,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
|