taro 1.4.0 → 2.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +89 -50
  4. data/lib/taro/common_returns.rb +31 -0
  5. data/lib/taro/declaration.rb +82 -0
  6. data/lib/taro/declarations.rb +34 -0
  7. data/lib/taro/errors.rb +15 -2
  8. data/lib/taro/export/base.rb +1 -1
  9. data/lib/taro/export/open_api_v3.rb +20 -23
  10. data/lib/taro/export.rb +1 -1
  11. data/lib/taro/none.rb +2 -0
  12. data/lib/taro/rails/active_declarations.rb +2 -10
  13. data/lib/taro/rails/declaration.rb +9 -114
  14. data/lib/taro/rails/declaration_buffer.rb +2 -1
  15. data/lib/taro/rails/dsl.rb +13 -6
  16. data/lib/taro/rails/generators/templates/response_type.erb +4 -0
  17. data/lib/taro/rails/generators.rb +1 -1
  18. data/lib/taro/rails/normalized_route.rb +20 -38
  19. data/lib/taro/rails/param_parsing.rb +5 -3
  20. data/lib/taro/rails/response_validator.rb +51 -50
  21. data/lib/taro/rails/route_finder.rb +1 -1
  22. data/lib/taro/rails/tasks/export.rake +10 -9
  23. data/lib/taro/rails.rb +2 -3
  24. data/lib/taro/return_def.rb +43 -0
  25. data/lib/taro/route.rb +32 -0
  26. data/lib/taro/status_code.rb +16 -0
  27. data/lib/taro/types/base_type.rb +6 -1
  28. data/lib/taro/types/field.rb +16 -4
  29. data/lib/taro/types/field_def.rb +62 -0
  30. data/lib/taro/types/field_validation.rb +4 -6
  31. data/lib/taro/types/input_type.rb +4 -9
  32. data/lib/taro/types/nested_response_type.rb +16 -0
  33. data/lib/taro/types/object_type.rb +2 -7
  34. data/lib/taro/types/object_types/no_content_type.rb +1 -5
  35. data/lib/taro/types/object_types/page_info_type.rb +1 -1
  36. data/lib/taro/types/object_types/page_type.rb +1 -5
  37. data/lib/taro/types/response_type.rb +8 -0
  38. data/lib/taro/types/scalar/integer_param_type.rb +15 -0
  39. data/lib/taro/types/scalar_type.rb +1 -1
  40. data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
  41. data/lib/taro/types/shared/derived_types.rb +34 -15
  42. data/lib/taro/types/shared/equivalence.rb +15 -0
  43. data/lib/taro/types/shared/errors.rb +8 -8
  44. data/lib/taro/types/shared/fields.rb +10 -36
  45. data/lib/taro/types/shared/name.rb +14 -0
  46. data/lib/taro/types/shared/object_coercion.rb +0 -13
  47. data/lib/taro/types/shared/openapi_name.rb +0 -6
  48. data/lib/taro/types/shared.rb +1 -1
  49. data/lib/taro/types.rb +1 -1
  50. data/lib/taro/version.rb +1 -1
  51. data/lib/taro.rb +6 -1
  52. metadata +16 -2
@@ -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
@@ -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
 
@@ -4,62 +4,67 @@ Taro::Rails::ResponseValidator = Struct.new(:controller, :declaration, :rendered
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).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
@@ -19,7 +19,7 @@ module Taro::Rails::RouteFinder
19
19
  # Build a Hash like
20
20
  # { 'users#show' } => [#<NormalizedRoute>, #<NormalizedRoute>] }
21
21
  rails_routes.each_with_object({}) do |rails_route, hash|
22
- route = Taro::Rails::NormalizedRoute.new(rails_route:)
22
+ route = Taro::Rails::NormalizedRoute.new(rails_route)
23
23
  next if route.ignored?
24
24
 
25
25
  (hash[route.endpoint] ||= []) << route
@@ -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
@@ -6,12 +6,17 @@
6
6
  # Instances of types are initialized with the object that they represent.
7
7
  # The object is a parameter hash for inputs and a manually passed hash
8
8
  # or object when rendering a response.
9
- Taro::Types::BaseType = Data.define(:object) do
9
+ #
10
+ # Using Struct instead of Data here for performance reasons:
11
+ # https://bugs.ruby-lang.org/issues/19693
12
+ Taro::Types::BaseType = Struct.new(:object) do
10
13
  require_relative "shared"
11
14
  extend Taro::Types::Shared::AdditionalProperties
12
15
  extend Taro::Types::Shared::Deprecation
13
16
  extend Taro::Types::Shared::DerivedTypes
14
17
  extend Taro::Types::Shared::Description
18
+ extend Taro::Types::Shared::Equivalence
19
+ extend Taro::Types::Shared::Name
15
20
  extend Taro::Types::Shared::OpenAPIName
16
21
  extend Taro::Types::Shared::OpenAPIType
17
22
  extend Taro::Types::Shared::Rendering
@@ -2,8 +2,9 @@ require_relative 'field_validation'
2
2
 
3
3
  Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum, :defined_at, :desc, :deprecated) do
4
4
  include Taro::Types::FieldValidation
5
+ include Taro::Types::Shared::Errors
5
6
 
6
- def initialize(name:, type:, null:, method: name, default: :none, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
7
+ def initialize(name:, type:, null:, method: name, default: Taro::None, enum: nil, defined_at: nil, desc: nil, deprecated: nil)
7
8
  enum = coerce_to_enum(enum)
8
9
  super(name:, type:, null:, method:, default:, enum:, defined_at:, desc:, deprecated:)
9
10
  end
@@ -21,7 +22,7 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
21
22
  end
22
23
 
23
24
  def default_specified?
24
- !default.equal?(:none)
25
+ !default.equal?(Taro::None)
25
26
  end
26
27
 
27
28
  def openapi_type
@@ -47,8 +48,7 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
47
48
  elsif object.respond_to?(method, true)
48
49
  object.public_send(method)
49
50
  else
50
- # Note that the ObjectCoercion module rescues this and adds context.
51
- raise Taro::ResponseError, "No such method or resolver `:#{method}`."
51
+ response_error "No such method or resolver `:#{method}`", object
52
52
  end
53
53
  end
54
54
 
@@ -66,5 +66,17 @@ Taro::Types::Field = Data.define(:name, :type, :null, :method, :default, :enum,
66
66
 
67
67
  type_obj = type.new(value)
68
68
  from_input ? type_obj.coerce_input : type_obj.coerce_response
69
+ rescue Taro::ValidationError => e
70
+ reraise_recursively_with_path_info(e)
71
+ end
72
+
73
+ def reraise_recursively_with_path_info(error)
74
+ msg =
75
+ error
76
+ .message
77
+ .sub(/ at `\K/, "#{name}.")
78
+ .sub(/(is not valid as [^`]+)(?=: )/, "\\1 at `#{name}`")
79
+
80
+ raise error.class.new(msg, error.object, error.origin)
69
81
  end
70
82
  end