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