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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +138 -60
  4. data/lib/taro/cache.rb +14 -0
  5. data/lib/taro/common_returns.rb +31 -0
  6. data/lib/taro/declaration.rb +82 -0
  7. data/lib/taro/declarations.rb +34 -0
  8. data/lib/taro/errors.rb +15 -2
  9. data/lib/taro/export/base.rb +1 -1
  10. data/lib/taro/export/open_api_v3.rb +20 -23
  11. data/lib/taro/export.rb +1 -1
  12. data/lib/taro/none.rb +2 -0
  13. data/lib/taro/rails/active_declarations.rb +2 -10
  14. data/lib/taro/rails/declaration.rb +9 -114
  15. data/lib/taro/rails/declaration_buffer.rb +2 -1
  16. data/lib/taro/rails/dsl.rb +13 -6
  17. data/lib/taro/rails/generators/install_generator.rb +1 -1
  18. data/lib/taro/rails/generators/templates/errors_type.erb +1 -1
  19. data/lib/taro/rails/generators/templates/response_type.erb +4 -0
  20. data/lib/taro/rails/generators.rb +1 -1
  21. data/lib/taro/rails/normalized_route.rb +20 -38
  22. data/lib/taro/rails/param_parsing.rb +5 -3
  23. data/lib/taro/rails/railtie.rb +4 -0
  24. data/lib/taro/rails/response_validator.rb +53 -52
  25. data/lib/taro/rails/route_finder.rb +5 -7
  26. data/lib/taro/rails/tasks/export.rake +10 -9
  27. data/lib/taro/rails.rb +2 -3
  28. data/lib/taro/return_def.rb +43 -0
  29. data/lib/taro/route.rb +32 -0
  30. data/lib/taro/status_code.rb +16 -0
  31. data/lib/taro/types/base_type.rb +7 -1
  32. data/lib/taro/types/coercion.rb +2 -2
  33. data/lib/taro/types/enum_type.rb +1 -1
  34. data/lib/taro/types/field.rb +17 -5
  35. data/lib/taro/types/field_def.rb +62 -0
  36. data/lib/taro/types/field_validation.rb +4 -6
  37. data/lib/taro/types/input_type.rb +4 -9
  38. data/lib/taro/types/list_type.rb +1 -1
  39. data/lib/taro/types/nested_response_type.rb +16 -0
  40. data/lib/taro/types/object_type.rb +2 -7
  41. data/lib/taro/types/object_types/no_content_type.rb +1 -5
  42. data/lib/taro/types/object_types/page_info_type.rb +1 -1
  43. data/lib/taro/types/object_types/page_type.rb +1 -5
  44. data/lib/taro/types/response_type.rb +8 -0
  45. data/lib/taro/types/scalar/integer_param_type.rb +15 -0
  46. data/lib/taro/types/scalar_type.rb +1 -1
  47. data/lib/taro/types/shared/caching.rb +30 -0
  48. data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
  49. data/lib/taro/types/shared/derived_types.rb +34 -15
  50. data/lib/taro/types/shared/equivalence.rb +14 -0
  51. data/lib/taro/types/shared/errors.rb +8 -8
  52. data/lib/taro/types/shared/fields.rb +10 -36
  53. data/lib/taro/types/shared/name.rb +14 -0
  54. data/lib/taro/types/shared/object_coercion.rb +0 -13
  55. data/lib/taro/types/shared/openapi_name.rb +0 -6
  56. data/lib/taro/types/shared/rendering.rb +5 -3
  57. data/lib/taro/types/shared.rb +1 -1
  58. data/lib/taro/types.rb +1 -1
  59. data/lib/taro/version.rb +1 -1
  60. data/lib/taro.rb +6 -1
  61. metadata +19 -3
data/lib/taro/export.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Taro::Export
2
- Dir[File.join(__dir__, "export", "*.rb")].each { |f| require f }
2
+ Dir[File.join(__dir__, "export", "*.rb")].each { |f| require_relative f }
3
3
  end
data/lib/taro/none.rb ADDED
@@ -0,0 +1,2 @@
1
+ # placeholder for not-given keyword arguments
2
+ Taro::None = Object.new
@@ -1,19 +1,11 @@
1
1
  module Taro::Rails::ActiveDeclarations
2
2
  def apply(declaration:, controller_class:, action_name:)
3
- (declarations_map[controller_class] ||= {})[action_name] = declaration
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
- declarations_map[controller.class].to_h[controller.action_name.to_sym]
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 :desc, :summary, :params, :return_defs, :return_descriptions, :return_nestings, :routes, :tags
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
- routes = Taro::Rails::RouteFinder.call(controller_class:, action_name:)
57
- routes.any? || raise_missing_route(controller_class, action_name)
58
- self.routes = routes
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 raise_missing_route(controller_class, action_name)
116
- raise(Taro::ArgumentError, "No route found for #{controller_class}##{action_name}")
10
+ def endpoint
11
+ action_name && "#{controller_class}##{action_name}"
117
12
  end
118
13
 
119
- def <=>(other)
120
- routes.first.openapi_operation_id <=> other.routes.first.openapi_operation_id
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] ||= Taro::Rails::Declaration.new
5
+ buffered_declarations[controller_class] ||=
6
+ Taro::Rails::Declaration.new(controller_class)
6
7
  end
7
8
 
8
9
  def buffered_declarations
@@ -1,18 +1,25 @@
1
1
  module Taro::Rails::DSL
2
- def api(summary, **kwargs)
3
- Taro::Rails.buffered_declaration(self).add_info(summary, **kwargs)
2
+ def api(summary, **)
3
+ Taro::Rails.buffered_declaration(self).add_info(summary, **)
4
4
  end
5
5
 
6
- def param(param_name, **kwargs)
7
- Taro::Rails.buffered_declaration(self).add_param(param_name, **kwargs)
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(field_name = nil, **kwargs)
11
- Taro::Rails.buffered_declaration(self).add_return(field_name, **kwargs)
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).sub('erb', 'rb')}"
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).coerce_response }
28
+ list.map { |el| self.class.item_type.new(el).cached_coerce_response }
29
29
  end
30
30
  end
@@ -0,0 +1,4 @@
1
+ # This file is generated by taro so you don't have to inherit from
2
+ # Taro::Types::ResponseType directly. You can customize or delete it.
3
+ class ResponseType < Taro::Types::ResponseType
4
+ end
@@ -1,3 +1,3 @@
1
1
  module Taro::Rails::Generators
2
- Dir[File.join(__dir__, 'generators', '**', '*.rb')].each { |f| require f }
2
+ Dir[File.join(__dir__, 'generators', '**', '*.rb')].each { |f| require_relative f }
3
3
  end
@@ -1,49 +1,31 @@
1
- Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
2
- def ignored?
3
- verb.to_s.empty? || patch_update?
4
- end
1
+ require_relative '../route'
5
2
 
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
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
- def openapi_operation_id
22
- "#{verb}_#{action}_#{controller.gsub('/', '__')}"
13
+ super(endpoint:, openapi_operation_id:, openapi_path:, verb:)
23
14
  end
24
15
 
25
- def path_params
26
- openapi_path.scan(/{(\w+)}/).flatten.map(&:to_sym)
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
- def controller
38
- rails_route.requirements[:controller]
39
- end
20
+ private
40
21
 
41
- def can_have_request_body?
42
- %w[patch post put].include?(verb)
22
+ # Internal routes of rails sometimes have no verb.
23
+ def internal?
24
+ verb.empty?
43
25
  end
44
26
 
45
- def inspect
46
- %(#<#{self.class} "#{verb} #{openapi_path}">)
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.before_action(only: action_name) do
11
- declaration = Taro::Rails.declaration_for(self)
12
- @api_params = declaration.parse_params(params)
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
 
@@ -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(*args)
3
- new(*args).call
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 < Taro::Types::ScalarType
8
- check_scalar
9
- elsif declared_return_type < Taro::Types::ListType &&
10
- declared_return_type.item_type < Taro::Types::ScalarType
11
- check_scalar_array
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
- check_custom_type
13
+ check(declared_return_type, rendered)
16
14
  end
17
15
  end
18
16
 
19
17
  def declared_return_type
20
- @declared_return_type ||= begin
21
- return_type = declaration.returns[controller.status] ||
22
- fail_with('No return type declared for this status.')
23
- nesting ? return_type.fields.fetch(nesting).type : return_type
24
- end
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, <<~MSG
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'` (ad-hoc return type)
36
- def nesting
37
- @nesting ||= declaration.return_nestings[controller.status]
38
- end
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
- fail_with_nesting_error
48
+ fail_with "Expected key :#{nesting}, got: #{rendered.keys}."
49
49
  end
50
50
  end
51
51
 
52
- def assert_rendered_is_a_hash
53
- rendered.is_a?(Hash) || fail_with("Expected Hash, got #{rendered.class}.")
54
- end
55
-
56
- def fail_with_nesting_error
57
- fail_with "Expected key :#{nesting}, got: #{rendered.keys}."
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 = declared_return_type, value = subject)
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 subject
71
- @subject ||= nesting ? denest_rendered : rendered
72
- end
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
- declared_return_type.new(subject).coerce_response
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 declared_return_type <= Taro::Types::ObjectTypes::FreeFormType
92
- return if declared_return_type <= Taro::Types::ObjectTypes::NoContentType
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 = declared_return_type.last_render
99
- used_type&.<=(declared_return_type) || fail_with(<<~MSG)
100
- Expected to use #{declared_return_type}.render, but the last type rendered
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 == subject.__id__ || fail_with(<<~MSG)
105
- #{declared_return_type}.render was called, but the result
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' } => [#<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
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
- data = export.send("to_#{Taro.config.export_format}")
12
+ export = Taro::Export::OpenAPIv3.call(title:, version:)
14
13
 
15
- FileUtils.mkdir_p(File.dirname(Taro.config.export_path))
16
- File.write(Taro.config.export_path, data)
14
+ data = export.send("to_#{format}")
17
15
 
18
- puts "Exported #{Taro.config.api_name} to #{Taro.config.export_path}"
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| require 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::Types::BaseType.last_render = nil
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