taro 1.4.0 → 2.1.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/CHANGELOG.md +35 -0
- data/README.md +138 -60
- data/lib/taro/cache.rb +14 -0
- data/lib/taro/common_returns.rb +31 -0
- data/lib/taro/declaration.rb +82 -0
- data/lib/taro/declarations.rb +34 -0
- data/lib/taro/errors.rb +15 -2
- data/lib/taro/export/base.rb +1 -1
- data/lib/taro/export/open_api_v3.rb +20 -23
- data/lib/taro/export.rb +1 -1
- data/lib/taro/none.rb +2 -0
- data/lib/taro/rails/active_declarations.rb +2 -10
- data/lib/taro/rails/declaration.rb +9 -114
- data/lib/taro/rails/declaration_buffer.rb +2 -1
- data/lib/taro/rails/dsl.rb +13 -6
- data/lib/taro/rails/generators/install_generator.rb +1 -1
- data/lib/taro/rails/generators/templates/errors_type.erb +1 -1
- data/lib/taro/rails/generators/templates/response_type.erb +4 -0
- data/lib/taro/rails/generators.rb +1 -1
- data/lib/taro/rails/normalized_route.rb +20 -38
- data/lib/taro/rails/param_parsing.rb +5 -3
- data/lib/taro/rails/railtie.rb +4 -0
- data/lib/taro/rails/response_validator.rb +53 -52
- data/lib/taro/rails/route_finder.rb +5 -7
- data/lib/taro/rails/tasks/export.rake +10 -9
- data/lib/taro/rails.rb +2 -3
- data/lib/taro/return_def.rb +43 -0
- data/lib/taro/route.rb +32 -0
- data/lib/taro/status_code.rb +16 -0
- data/lib/taro/types/base_type.rb +7 -1
- data/lib/taro/types/coercion.rb +2 -2
- data/lib/taro/types/enum_type.rb +1 -1
- data/lib/taro/types/field.rb +17 -5
- data/lib/taro/types/field_def.rb +62 -0
- data/lib/taro/types/field_validation.rb +4 -6
- data/lib/taro/types/input_type.rb +4 -9
- data/lib/taro/types/list_type.rb +1 -1
- data/lib/taro/types/nested_response_type.rb +16 -0
- data/lib/taro/types/object_type.rb +2 -7
- data/lib/taro/types/object_types/no_content_type.rb +1 -5
- data/lib/taro/types/object_types/page_info_type.rb +1 -1
- data/lib/taro/types/object_types/page_type.rb +1 -5
- data/lib/taro/types/response_type.rb +8 -0
- data/lib/taro/types/scalar/integer_param_type.rb +15 -0
- data/lib/taro/types/scalar_type.rb +1 -1
- data/lib/taro/types/shared/caching.rb +30 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
- data/lib/taro/types/shared/derived_types.rb +34 -15
- data/lib/taro/types/shared/equivalence.rb +14 -0
- data/lib/taro/types/shared/errors.rb +8 -8
- data/lib/taro/types/shared/fields.rb +10 -36
- data/lib/taro/types/shared/name.rb +14 -0
- data/lib/taro/types/shared/object_coercion.rb +0 -13
- data/lib/taro/types/shared/openapi_name.rb +0 -6
- data/lib/taro/types/shared/rendering.rb +5 -3
- data/lib/taro/types/shared.rb +1 -1
- data/lib/taro/types.rb +1 -1
- data/lib/taro/version.rb +1 -1
- data/lib/taro.rb +6 -1
- metadata +19 -3
data/lib/taro/export.rb
CHANGED
data/lib/taro/none.rb
ADDED
@@ -1,19 +1,11 @@
|
|
1
1
|
module Taro::Rails::ActiveDeclarations
|
2
2
|
def apply(declaration:, controller_class:, action_name:)
|
3
|
-
|
3
|
+
Taro.declarations["#{controller_class.name}##{action_name}"] = declaration
|
4
4
|
Taro::Rails::ParamParsing.install(controller_class:, action_name:)
|
5
5
|
Taro::Rails::ResponseValidation.install(controller_class:)
|
6
6
|
end
|
7
7
|
|
8
|
-
def declarations_map
|
9
|
-
@declarations_map ||= {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def declarations
|
13
|
-
declarations_map.values.flat_map(&:values)
|
14
|
-
end
|
15
|
-
|
16
8
|
def declaration_for(controller)
|
17
|
-
|
9
|
+
Taro.declarations["#{controller.class.name}##{controller.action_name}"]
|
18
10
|
end
|
19
11
|
end
|
@@ -1,122 +1,17 @@
|
|
1
|
-
class Taro::Rails::Declaration
|
2
|
-
attr_reader :
|
3
|
-
|
4
|
-
def initialize
|
5
|
-
@params = Class.new(Taro::Types::InputType)
|
6
|
-
@return_defs = {}
|
7
|
-
@return_descriptions = {}
|
8
|
-
@return_nestings = {}
|
9
|
-
end
|
10
|
-
|
11
|
-
def add_info(summary, desc: nil, tags: nil)
|
12
|
-
summary.is_a?(String) || raise(Taro::ArgumentError, 'api summary must be a String')
|
13
|
-
@summary = summary
|
14
|
-
@desc = desc
|
15
|
-
@tags = Array(tags) if tags
|
16
|
-
end
|
17
|
-
|
18
|
-
def add_param(param_name, **kwargs)
|
19
|
-
kwargs[:defined_at] = caller_locations(1..2)[1]
|
20
|
-
@params.field(param_name, **kwargs)
|
21
|
-
end
|
22
|
-
|
23
|
-
def add_return(nesting = nil, code:, desc: nil, **kwargs)
|
24
|
-
status = self.class.coerce_status_to_int(code)
|
25
|
-
raise_if_already_declared(status)
|
26
|
-
|
27
|
-
kwargs[:nesting] = nesting
|
28
|
-
check_return_kwargs(kwargs)
|
29
|
-
|
30
|
-
kwargs[:defined_at] = caller_locations(1..2)[1]
|
31
|
-
return_defs[status] = kwargs
|
32
|
-
|
33
|
-
# response desc is required in openapi 3 – fall back to status code
|
34
|
-
return_descriptions[status] = desc || code.to_s
|
35
|
-
|
36
|
-
# if a field name is provided, the response should be nested
|
37
|
-
return_nestings[status] = nesting if nesting
|
38
|
-
end
|
39
|
-
|
40
|
-
# Return types are evaluated lazily to avoid unnecessary autoloading
|
41
|
-
# of all types in dev/test envs.
|
42
|
-
def returns
|
43
|
-
@returns ||= evaluate_return_defs
|
44
|
-
end
|
45
|
-
|
46
|
-
def raise_if_already_declared(status)
|
47
|
-
return_defs[status] &&
|
48
|
-
raise(Taro::ArgumentError, "response for status #{status} already declared")
|
49
|
-
end
|
50
|
-
|
51
|
-
def parse_params(rails_params)
|
52
|
-
params.new(rails_params.to_unsafe_h).coerce_input
|
53
|
-
end
|
1
|
+
class Taro::Rails::Declaration < Taro::Declaration
|
2
|
+
attr_reader :controller_class, :action_name
|
54
3
|
|
55
4
|
def finalize(controller_class:, action_name:)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
60
|
-
|
61
|
-
def routes=(arg)
|
62
|
-
arg.is_a?(Array) || raise(Taro::ArgumentError, 'routes must be an Array')
|
63
|
-
@routes = arg
|
64
|
-
end
|
65
|
-
|
66
|
-
def polymorphic_route?
|
67
|
-
routes.size > 1
|
68
|
-
end
|
69
|
-
|
70
|
-
require 'rack'
|
71
|
-
def self.coerce_status_to_int(status)
|
72
|
-
# support using http status numbers directly
|
73
|
-
return status if ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(status)
|
74
|
-
|
75
|
-
# support using symbols, but coerce them to numbers
|
76
|
-
::Rack::Utils::SYMBOL_TO_STATUS_CODE[status] ||
|
77
|
-
raise(Taro::ArgumentError, "Invalid status: #{status.inspect}")
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def check_return_kwargs(kwargs)
|
83
|
-
# For nested returns, evaluate_return_def calls ::field, which validates
|
84
|
-
# field options, but does not trigger type autoloading.
|
85
|
-
return evaluate_return_def(**kwargs) if kwargs[:nesting]
|
86
|
-
|
87
|
-
if kwargs.key?(:null)
|
88
|
-
raise Taro::ArgumentError, <<~MSG
|
89
|
-
`null:` is not supported for top-level returns. If you want a nullable return
|
90
|
-
value, nest it, e.g. `returns :str, type: 'String', null: true`.
|
91
|
-
MSG
|
92
|
-
end
|
93
|
-
|
94
|
-
bad_keys = kwargs.keys - (Taro::Types::Coercion.keys + %i[code desc nesting])
|
95
|
-
return if bad_keys.empty?
|
96
|
-
|
97
|
-
raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
|
98
|
-
end
|
99
|
-
|
100
|
-
def evaluate_return_defs
|
101
|
-
return_defs.transform_values { |defi| evaluate_return_def(**defi) }
|
102
|
-
end
|
103
|
-
|
104
|
-
def evaluate_return_def(nesting:, **kwargs)
|
105
|
-
if nesting
|
106
|
-
# ad-hoc return type, requiring the actual return type to be nested
|
107
|
-
Class.new(Taro::Types::ObjectType).tap do |type|
|
108
|
-
type.field(nesting, null: false, **kwargs)
|
109
|
-
end
|
110
|
-
else
|
111
|
-
Taro::Types::Coercion.call(kwargs)
|
112
|
-
end
|
5
|
+
@controller_class = controller_class
|
6
|
+
@action_name = action_name
|
7
|
+
@params.define_name("InputType(#{endpoint})")
|
113
8
|
end
|
114
9
|
|
115
|
-
def
|
116
|
-
|
10
|
+
def endpoint
|
11
|
+
action_name && "#{controller_class}##{action_name}"
|
117
12
|
end
|
118
13
|
|
119
|
-
def
|
120
|
-
routes
|
14
|
+
def routes
|
15
|
+
@routes ||= Taro::Rails::RouteFinder.call(controller_class:, action_name:)
|
121
16
|
end
|
122
17
|
end
|
@@ -2,7 +2,8 @@
|
|
2
2
|
# until the next action method is defined (e.g. `def create`).
|
3
3
|
module Taro::Rails::DeclarationBuffer
|
4
4
|
def buffered_declaration(controller_class)
|
5
|
-
buffered_declarations[controller_class] ||=
|
5
|
+
buffered_declarations[controller_class] ||=
|
6
|
+
Taro::Rails::Declaration.new(controller_class)
|
6
7
|
end
|
7
8
|
|
8
9
|
def buffered_declarations
|
data/lib/taro/rails/dsl.rb
CHANGED
@@ -1,18 +1,25 @@
|
|
1
1
|
module Taro::Rails::DSL
|
2
|
-
def api(summary, **
|
3
|
-
Taro::Rails.buffered_declaration(self).add_info(summary, **
|
2
|
+
def api(summary, **)
|
3
|
+
Taro::Rails.buffered_declaration(self).add_info(summary, **)
|
4
4
|
end
|
5
5
|
|
6
|
-
def param(param_name, **
|
7
|
-
|
6
|
+
def param(param_name, **)
|
7
|
+
defined_at = caller_locations(1..1)[0]
|
8
|
+
Taro::Rails.buffered_declaration(self).add_param(param_name, defined_at:, **)
|
8
9
|
end
|
9
10
|
|
10
|
-
def returns(
|
11
|
-
|
11
|
+
def returns(nesting = nil, **)
|
12
|
+
defined_at = caller_locations(1..1)[0]
|
13
|
+
Taro::Rails.buffered_declaration(self).add_return(nesting, defined_at:, **)
|
12
14
|
end
|
13
15
|
|
14
16
|
def method_added(method_name)
|
15
17
|
Taro::Rails.apply_buffered_declaration(self, method_name)
|
16
18
|
super
|
17
19
|
end
|
20
|
+
|
21
|
+
def common_return(nesting = nil, **)
|
22
|
+
defined_at = caller_locations(1..1)[0]
|
23
|
+
Taro::CommonReturns.define(self, nesting, defined_at:, **)
|
24
|
+
end
|
18
25
|
end
|
@@ -12,7 +12,7 @@ class Taro::Rails::Generators::InstallGenerator < ::Rails::Generators::Base
|
|
12
12
|
def create_type_files
|
13
13
|
Dir["#{self.class.source_root}/**/*.erb"].each do |tmpl|
|
14
14
|
dest_dir = options[:dir].chomp('/')
|
15
|
-
template tmpl, "#{dest_dir}/#{File.basename(tmpl
|
15
|
+
template tmpl, "#{dest_dir}/#{File.basename(tmpl, '.erb')}.rb"
|
16
16
|
end
|
17
17
|
end
|
18
18
|
# :nocov:
|
@@ -25,6 +25,6 @@ class ErrorsType < Taro::Types::ListType
|
|
25
25
|
response_error("must be an Enumerable or an object with errors")
|
26
26
|
end
|
27
27
|
|
28
|
-
list.map { |el| self.class.item_type.new(el).
|
28
|
+
list.map { |el| self.class.item_type.new(el).cached_coerce_response }
|
29
29
|
end
|
30
30
|
end
|
@@ -1,49 +1,31 @@
|
|
1
|
-
|
2
|
-
def ignored?
|
3
|
-
verb.to_s.empty? || patch_update?
|
4
|
-
end
|
1
|
+
require_relative '../route'
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def openapi_path
|
18
|
-
rails_route.path.spec.to_s.gsub('(.:format)', '').gsub(/:(\w+)/, '{\1}')
|
19
|
-
end
|
3
|
+
class Taro::Rails::NormalizedRoute < Taro::Route
|
4
|
+
def initialize(rails_route)
|
5
|
+
action, controller = rails_route.requirements.values_at(:action, :controller)
|
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
|
+
verb = rails_route.verb.to_s.scan(/\w+/).sort.last.to_s.downcase
|
9
|
+
openapi_operation_id = "#{verb}_#{action}_#{controller}".gsub('/', '__')
|
10
|
+
openapi_path = rails_route.path.spec.to_s.gsub('(.:format)', '').gsub(/:(\w+)/, '{\1}')
|
11
|
+
endpoint = "#{controller}##{action}"
|
20
12
|
|
21
|
-
|
22
|
-
"#{verb}_#{action}_#{controller.gsub('/', '__')}"
|
13
|
+
super(endpoint:, openapi_operation_id:, openapi_path:, verb:)
|
23
14
|
end
|
24
15
|
|
25
|
-
def
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def endpoint
|
30
|
-
"#{controller}##{action}"
|
31
|
-
end
|
32
|
-
|
33
|
-
def action
|
34
|
-
rails_route.requirements[:action]
|
16
|
+
def ignored?
|
17
|
+
internal? || patch_update?
|
35
18
|
end
|
36
19
|
|
37
|
-
|
38
|
-
rails_route.requirements[:controller]
|
39
|
-
end
|
20
|
+
private
|
40
21
|
|
41
|
-
|
42
|
-
|
22
|
+
# Internal routes of rails sometimes have no verb.
|
23
|
+
def internal?
|
24
|
+
verb.empty?
|
43
25
|
end
|
44
26
|
|
45
|
-
|
46
|
-
|
27
|
+
# Rails has both PATCH and PUT routes for updates. We only need one copy.
|
28
|
+
def patch_update?
|
29
|
+
verb == 'patch' && endpoint.end_with?('#update')
|
47
30
|
end
|
48
|
-
alias to_s inspect
|
49
31
|
end
|
@@ -7,9 +7,11 @@ module Taro::Rails::ParamParsing
|
|
7
7
|
|
8
8
|
installed[key] = true
|
9
9
|
|
10
|
-
controller_class.
|
11
|
-
declaration = Taro::Rails.declaration_for(self)
|
12
|
-
|
10
|
+
controller_class.prepend_before_action(only: action_name) do
|
11
|
+
declaration = Taro::Rails.declaration_for(self) || raise(
|
12
|
+
Taro::InvariantError, "missing Declaration for #{controller_class}##{action_name}"
|
13
|
+
)
|
14
|
+
@api_params = declaration.params.new(params.to_unsafe_h).coerce_input
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
data/lib/taro/rails/railtie.rb
CHANGED
@@ -9,6 +9,10 @@ class Taro::Rails::Railtie < ::Rails::Railtie
|
|
9
9
|
app.reloader.to_prepare do
|
10
10
|
Taro::Rails.reset
|
11
11
|
end
|
12
|
+
|
13
|
+
app.config.after_initialize do
|
14
|
+
Taro::Cache.cache_instance = Rails.cache
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
rake_tasks { Dir["#{__dir__}/tasks/**/*.rake"].each { |f| load f } }
|
@@ -1,65 +1,70 @@
|
|
1
1
|
Taro::Rails::ResponseValidator = Struct.new(:controller, :declaration, :rendered) do
|
2
|
-
def self.call(
|
3
|
-
new(
|
2
|
+
def self.call(controller, declaration, rendered)
|
3
|
+
new(controller, declaration, rendered).call
|
4
4
|
end
|
5
5
|
|
6
6
|
def call
|
7
|
-
if declared_return_type
|
8
|
-
|
9
|
-
elsif declared_return_type < Taro::Types::
|
10
|
-
|
11
|
-
|
12
|
-
elsif declared_return_type < Taro::Types::EnumType
|
13
|
-
check_enum
|
7
|
+
if declared_return_type.nil?
|
8
|
+
fail_if_declaration_expected
|
9
|
+
elsif declared_return_type < Taro::Types::NestedResponseType
|
10
|
+
field = declared_return_type.nesting_field
|
11
|
+
check(field.type, denest_rendered(field.name))
|
14
12
|
else
|
15
|
-
|
13
|
+
check(declared_return_type, rendered)
|
16
14
|
end
|
17
15
|
end
|
18
16
|
|
19
17
|
def declared_return_type
|
20
|
-
@declared_return_type ||=
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
@declared_return_type ||= declaration.returns[controller.status]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Rack, Rails and gems commonly trigger rendering of 400, 404, 500 etc.
|
22
|
+
# Declaring these codes should be optional. Otherwise the api schema would get
|
23
|
+
# bloated as there are no "global" return declarations in OpenAPI v3, and we'd
|
24
|
+
# need to export all of these for every single endpoint. v4 might change this.
|
25
|
+
# https://github.com/OAI/OpenAPI-Specification/issues/521
|
26
|
+
def fail_if_declaration_expected
|
27
|
+
controller.status.to_s.match?(/^[123]|422/) && fail_with(<<~MSG)
|
28
|
+
No return type declared for this status.
|
29
|
+
MSG
|
25
30
|
end
|
26
31
|
|
27
32
|
def fail_with(message)
|
28
|
-
raise Taro::ResponseError,
|
33
|
+
raise Taro::ResponseError.new(<<~MSG, rendered, self)
|
29
34
|
Response validation error for
|
30
35
|
#{controller.class}##{controller.action_name}, code #{controller.status}":
|
31
36
|
#{message}
|
32
37
|
MSG
|
33
38
|
end
|
34
39
|
|
35
|
-
# support `returns :some_nesting, type: 'SomeType'`
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
def denest_rendered
|
41
|
-
assert_rendered_is_a_hash
|
40
|
+
# support `returns :some_nesting, type: 'SomeType'`
|
41
|
+
# used like `render json: { some_nesting: SomeType.render(some_object) }`
|
42
|
+
def denest_rendered(nesting)
|
43
|
+
rendered.is_a?(Hash) || fail_with("Expected Hash, got #{rendered.class}.")
|
42
44
|
|
43
45
|
if rendered.key?(nesting)
|
44
46
|
rendered[nesting]
|
45
|
-
elsif rendered.key?(nesting.to_s)
|
46
|
-
rendered[nesting.to_s]
|
47
47
|
else
|
48
|
-
|
48
|
+
fail_with "Expected key :#{nesting}, got: #{rendered.keys}."
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
52
|
+
def check(type, value)
|
53
|
+
if type < Taro::Types::ScalarType
|
54
|
+
check_scalar(type, value)
|
55
|
+
elsif type < Taro::Types::ListType &&
|
56
|
+
type.item_type < Taro::Types::ScalarType
|
57
|
+
check_scalar_array(type, value)
|
58
|
+
elsif type < Taro::Types::EnumType
|
59
|
+
check_enum(type, value)
|
60
|
+
else
|
61
|
+
check_custom_type(type, value)
|
62
|
+
end
|
58
63
|
end
|
59
64
|
|
60
65
|
# For scalar and enum types, we want to support e.g. `render json: 42`,
|
61
66
|
# and not require using the type as in `BeautifulNumbersEnum.render(42)`.
|
62
|
-
def check_scalar(type
|
67
|
+
def check_scalar(type, value)
|
63
68
|
case type.openapi_type
|
64
69
|
when :integer, :number then value.is_a?(Numeric)
|
65
70
|
when :string then value.is_a?(String) || value.is_a?(Symbol)
|
@@ -67,18 +72,14 @@ Taro::Rails::ResponseValidator = Struct.new(:controller, :declaration, :rendered
|
|
67
72
|
end || fail_with("Expected a #{type.openapi_type}, got: #{value.class}.")
|
68
73
|
end
|
69
74
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def check_scalar_array
|
75
|
-
subject.is_a?(Array) || fail_with('Expected an Array.')
|
76
|
-
subject.empty? || check_scalar(declared_return_type.item_type, subject.first)
|
75
|
+
def check_scalar_array(type, value)
|
76
|
+
value.is_a?(Array) || fail_with('Expected an Array.')
|
77
|
+
value.empty? || check_scalar(type.item_type, value.first)
|
77
78
|
end
|
78
79
|
|
79
|
-
def check_enum
|
80
|
+
def check_enum(type, value)
|
80
81
|
# coercion checks non-emptyness + enum match
|
81
|
-
|
82
|
+
type.new(value).cached_coerce_response
|
82
83
|
rescue Taro::Error => e
|
83
84
|
fail_with(e.message)
|
84
85
|
end
|
@@ -86,23 +87,23 @@ Taro::Rails::ResponseValidator = Struct.new(:controller, :declaration, :rendered
|
|
86
87
|
# For complex/object types, we ensure conformance by checking whether
|
87
88
|
# the type was used for rendering. This has performance benefits compared
|
88
89
|
# to going over the structure a second time.
|
89
|
-
def check_custom_type
|
90
|
+
def check_custom_type(type, value)
|
90
91
|
# Ignore types without a specified structure.
|
91
|
-
return if
|
92
|
-
return if
|
92
|
+
return if type <= Taro::Types::ObjectTypes::FreeFormType
|
93
|
+
return if type <= Taro::Types::ObjectTypes::NoContentType
|
93
94
|
|
94
|
-
strict_check_custom_type
|
95
|
+
strict_check_custom_type(type, value)
|
95
96
|
end
|
96
97
|
|
97
|
-
def strict_check_custom_type
|
98
|
-
used_type, rendered_object_id =
|
99
|
-
used_type
|
100
|
-
Expected to use #{
|
98
|
+
def strict_check_custom_type(type, value)
|
99
|
+
used_type, rendered_object_id = type.last_render
|
100
|
+
used_type == type || used_type&.<(type) || fail_with(<<~MSG)
|
101
|
+
Expected to use #{type}.render, but the last type rendered
|
101
102
|
was: #{used_type || 'no type'}.
|
102
103
|
MSG
|
103
104
|
|
104
|
-
rendered_object_id ==
|
105
|
-
#{
|
105
|
+
rendered_object_id == value.__id__ || fail_with(<<~MSG)
|
106
|
+
#{type}.render was called, but the result
|
106
107
|
of this call was not used in the response.
|
107
108
|
MSG
|
108
109
|
end
|
@@ -17,13 +17,11 @@ module Taro::Rails::RouteFinder
|
|
17
17
|
|
18
18
|
def build_cache
|
19
19
|
# Build a Hash like
|
20
|
-
# { 'users#show'
|
21
|
-
rails_routes
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
(hash[route.endpoint] ||= []) << route
|
26
|
-
end
|
20
|
+
# { 'users#show' => [#<NormalizedRoute>, #<NormalizedRoute>] }
|
21
|
+
rails_routes
|
22
|
+
.map { |r| Taro::Rails::NormalizedRoute.new(r) }
|
23
|
+
.reject(&:ignored?)
|
24
|
+
.group_by(&:endpoint)
|
27
25
|
end
|
28
26
|
|
29
27
|
def rails_routes
|
@@ -3,17 +3,18 @@ task 'taro:export' => :environment do
|
|
3
3
|
# make sure all declarations have been seen
|
4
4
|
Rails.application.eager_load!
|
5
5
|
|
6
|
+
title = Taro.config.api_name
|
7
|
+
version = Taro.config.api_version
|
8
|
+
format = Taro.config.export_format
|
9
|
+
path = Taro.config.export_path
|
6
10
|
# 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
11
|
|
13
|
-
|
12
|
+
export = Taro::Export::OpenAPIv3.call(title:, version:)
|
14
13
|
|
15
|
-
|
16
|
-
File.write(Taro.config.export_path, data)
|
14
|
+
data = export.send("to_#{format}")
|
17
15
|
|
18
|
-
|
16
|
+
FileUtils.mkdir_p(File.dirname(path))
|
17
|
+
File.write(path, data)
|
18
|
+
|
19
|
+
puts "Exported the API #{title} v#{version} to #{path}"
|
19
20
|
end
|
data/lib/taro/rails.rb
CHANGED
@@ -3,15 +3,14 @@ return unless defined?(::Rails)
|
|
3
3
|
# :nocov:
|
4
4
|
|
5
5
|
module Taro::Rails
|
6
|
-
Dir[File.join(__dir__, "rails", "*.rb")].each { |f|
|
6
|
+
Dir[File.join(__dir__, "rails", "*.rb")].each { |f| require_relative f }
|
7
7
|
|
8
8
|
extend ActiveDeclarations
|
9
9
|
extend DeclarationBuffer
|
10
10
|
|
11
11
|
def self.reset
|
12
12
|
buffered_declarations.clear
|
13
|
-
declarations_map.clear
|
14
13
|
RouteFinder.clear_cache
|
15
|
-
Taro
|
14
|
+
Taro.reset
|
16
15
|
end
|
17
16
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Lazily-evaluated response type definition.
|
2
|
+
class Taro::ReturnDef
|
3
|
+
attr_reader :code, :defined_at, :desc, :nesting, :params
|
4
|
+
|
5
|
+
def initialize(code:, defined_at: nil, desc: nil, nesting: nil, **params)
|
6
|
+
@code = Taro::StatusCode.coerce_to_int(code)
|
7
|
+
@defined_at = defined_at
|
8
|
+
@desc = desc
|
9
|
+
@nesting = nesting
|
10
|
+
@params = params
|
11
|
+
validate
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate
|
15
|
+
if nesting
|
16
|
+
Class.new(Taro::Types::NestedResponseType).tap do |type|
|
17
|
+
type.field(nesting, defined_at:, null: false, **params)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
Taro::Types::Coercion.call(params)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate
|
27
|
+
# For nested returns, call ::field, which validates
|
28
|
+
# field options, but does not trigger type auto-loading.
|
29
|
+
return evaluate if nesting
|
30
|
+
|
31
|
+
if params.key?(:null)
|
32
|
+
raise Taro::ArgumentError, <<~MSG
|
33
|
+
`null:` is not supported for top-level returns. If you want a nullable return
|
34
|
+
value, nest it, e.g. `returns :str, type: 'String', null: true`.
|
35
|
+
MSG
|
36
|
+
end
|
37
|
+
|
38
|
+
bad_keys = params.keys - (Taro::Types::Coercion.keys + %i[defined_at])
|
39
|
+
return if bad_keys.empty?
|
40
|
+
|
41
|
+
raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
|
42
|
+
end
|
43
|
+
end
|
data/lib/taro/route.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
class Taro::Route
|
2
|
+
attr_reader :endpoint, :openapi_operation_id, :openapi_path, :verb
|
3
|
+
|
4
|
+
def initialize(endpoint:, openapi_operation_id:, openapi_path:, verb:)
|
5
|
+
@endpoint = validate_string(endpoint:)
|
6
|
+
@openapi_operation_id = validate_string(openapi_operation_id:)
|
7
|
+
@openapi_path = validate_string(openapi_path:)
|
8
|
+
@verb = validate_string(verb:).downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def path_params
|
12
|
+
openapi_path.scan(/{(\w+)}/).flatten.map(&:to_sym)
|
13
|
+
end
|
14
|
+
|
15
|
+
def can_have_request_body?
|
16
|
+
%w[patch post put].include?(verb)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
%(#<#{self.class} "#{verb} #{openapi_path}">)
|
21
|
+
end
|
22
|
+
alias to_s inspect
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_string(**kwarg)
|
27
|
+
name, arg = kwarg.first
|
28
|
+
return arg if arg.is_a?(String)
|
29
|
+
|
30
|
+
raise(Taro::ArgumentError, "#{name} must be a String, got #{arg.class}")
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Taro::StatusCode
|
4
|
+
def self.coerce_to_int(arg)
|
5
|
+
# support using http status numbers directly
|
6
|
+
return arg if ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(arg)
|
7
|
+
|
8
|
+
# support using symbols, but coerce them to numbers
|
9
|
+
::Rack::Utils::SYMBOL_TO_STATUS_CODE[arg] ||
|
10
|
+
raise(Taro::ArgumentError, "Invalid status: #{arg.inspect}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.coerce_to_message(arg)
|
14
|
+
::Rack::Utils::HTTP_STATUS_CODES.fetch(coerce_to_int(arg))
|
15
|
+
end
|
16
|
+
end
|