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