taro 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d30f413c950e02e52f99ed96d234acdba122c92ec4e39dd2d35e87c899ed8a13
4
- data.tar.gz: 5ed455623397ce6fbcb19412710bf98bc7ca2b6898db193a2cf858104a149327
3
+ metadata.gz: 62127cdef8f44e34831cb4dec7d900bfa25c3c00994e750624de1f486c6880ab
4
+ data.tar.gz: 40bd31112d3e8fb3fe96bfa282acd6ad909c60f66431aa556e01052b3098ab44
5
5
  SHA512:
6
- metadata.gz: ba99529dd914238371708a80b5f3f6497a4884a042a0e11ec7e9b557107379d0bfa2232b6a71137623658651e5d920f8ad47ab683119ae5ce105261fa627b980
7
- data.tar.gz: 26b2652184c485aa7970707373cfe4ebfdecd3141fc7e995066060b5a0a5452b1e749d6e16fdc3d99e34a199434ed9edc7400d89fe8f8ed32df442d764d06175
6
+ metadata.gz: a609041860bc3516a70a1ebd2f073d031cac39a04f9ff5562ff7c6d0711b1c5dc3a900cfa4842ff77f6ab573ab0fa2164282cf68fff58133071a7b842201cecc
7
+ data.tar.gz: 4942e27cdc067cb5c46ae7f726743d1cf54282536195b892d93507757820e0c53db46ebfb5a40ad25cc13c60ef2eac6b493cb1e2d8b9cfa195a0db782734d765
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.0.0] - 2024-12-15
4
+
5
+ ### Changed
6
+
7
+ - rendering undeclared http error codes (except 422) is now allowed
8
+ - this previously raised errors when done in endpoints with other declarations
9
+ - as a result, some errors rendered from `rescue_from` blocks became 500s
10
+ - deduplicated response schemas for ad-hoc nested returns in OpenAPI export
11
+ - this only affects nested returns e.g. `returns :x, code: :ok, type: 'YType'`
12
+ - old name: `get_show_ys_200_Response`, new_name: `Y_in_x_Response`
13
+ - removed option to render nested returns with string keys
14
+ - e.g. for `returns :foo, [...]`, `render json: { 'foo' => [...] }` fails now
15
+ - removed `Taro::Rails.declarations`, replaced it with `Taro.declarations`
16
+
17
+ ### Added
18
+
19
+ - added `::common_return` to define common return types
20
+ - added support for declaring path & query params as Integer
21
+ - e.g. `param :id, type: 'Integer', required: true` for `/users/1`
22
+ - e.g. `param :page, type: 'Integer', required: true` for `?page=1`
23
+ - added parsed/rendered object to validation errors for debugging
24
+ - improved validation error messages
25
+
26
+ ### Fixed
27
+
28
+ - fixed unnecessary `$LOAD_PATH` searches at require time
29
+
3
30
  ## [1.4.0] - 2024-11-27
4
31
 
5
32
  ### Added
data/README.md CHANGED
@@ -32,25 +32,31 @@ This is how type classes can be used in a Rails controller:
32
32
  class BikesController < ApplicationController
33
33
  # This adds an endpoint summary, description, and tags to the docs (all optional)
34
34
  api 'Update a bike', desc: 'My longer text', tags: ['Bikes']
35
- # Params can come from the path, e.g. /bike/:id)
35
+
36
+ # Params can come from the path, e.g. /bike/:id.
37
+ # Some types, like UUID in this case, are predefined. See below for more.
36
38
  param :id, type: 'UUID', null: false, desc: 'ID of the bike to update'
37
- # They can also come from the query string or request body
39
+
40
+ # Params can also come from the query string or request body.
41
+ # This describes a Hash param:
38
42
  param :bike, type: 'BikeInputType', null: false
39
- # Return types can differ by status code and can be nested as in this case:
40
- returns :bike, code: :ok, type: 'BikeType', desc: 'update success'
41
- # This one is not nested:
42
- returns code: :unprocessable_content, type: 'MyErrorType', desc: 'failure'
43
+
44
+ # Return types can differ by status code:
45
+ returns code: :ok, type: 'BikeType', desc: 'update success'
46
+
47
+ # Return types can also be nested (in this case in an "error" key):
48
+ returns :error, code: :unprocessable_content, type: 'MyErrorType'
49
+
43
50
  def update
44
- # defined params are available as @api_params
51
+ # Declared params are available as @api_params
45
52
  bike = Bike.find(@api_params[:id])
46
53
  success = bike.update(@api_params[:bike])
47
54
 
48
- # Types can be used to render responses.
49
- # The object
55
+ # Types are also used to render responses.
50
56
  if success
51
- render json: { bike: BikeType.render(bike) }, status: :ok
57
+ render json: BikeType.render(bike), status: :ok
52
58
  else
53
- render json: MyErrorType.render(bike.errors.first), status: :unprocessable_entity
59
+ render json: { error: MyErrorType.render(bike.errors.first) }, status: :unprocessable_entity
54
60
  end
55
61
  end
56
62
 
@@ -63,18 +69,18 @@ class BikesController < ApplicationController
63
69
  end
64
70
  ```
65
71
 
66
- Notice the multiple roles of types: They are used to define the structure of API requests and responses, and render the response. Both the input and output of the API can be validated against the schema if desired (see below).
72
+ Notice the multiple roles of types: They are used to describe the structure of API requests and responses, and render the response. Both the input and output of the API are validated against the schema by default (see below).
67
73
 
68
- Here is an example of the `BikeType` from that controller:
74
+ Here is an example of the `BikeType` from the controller above:
69
75
 
70
76
  ```ruby
71
77
  class BikeType < ObjectType
72
- # Optional description of BikeType (for API docs and the OpenAPI export)
78
+ # Optional description of BikeType (for the OpenAPI export)
73
79
  self.desc = 'A bike and all relevant information about it'
74
80
 
75
81
  # Object types have fields. Each field has a name, its own type,
76
82
  # and a `null:` setting to indicate if it can be nil.
77
- # Providing a desc is optional.
83
+ # Providing a description is optional.
78
84
  field :brand, type: 'String', null: true, desc: 'The brand name'
79
85
 
80
86
  # Fields can reference other types and arrays of values
@@ -95,7 +101,9 @@ class BikeType < ObjectType
95
101
  end
96
102
  ```
97
103
 
98
- ### Input types
104
+ ### Input and response types
105
+
106
+ You can use object types for input and output. However, if you want added control, you can also define dedicated input and response types.
99
107
 
100
108
  Note the use of `BikeInputType` in the `param` declaration above? It could look like so:
101
109
 
@@ -106,7 +114,18 @@ class BikeInputType < InputType
106
114
  end
107
115
  ```
108
116
 
109
- The usage of such dedicated InputTypes is optional. Object types can also be used to define accepted parameters, or parts of them, depending on what you want to allow API clients to send.
117
+ A type inheriting from InputType behaves just like an ObjectType when parsing parameters. The only difference is that it can't be used in response declarations.
118
+
119
+ Likewise, there is a special type for responses, which can't be used in input declarations:
120
+
121
+ ```ruby
122
+ class BikeSearchResponseType < ResponseType
123
+ field :bike, type: 'BikeType', null: true, desc: 'The found bike'
124
+ field :search_duration, type: 'Integer', null: false
125
+ end
126
+ ```
127
+
128
+ In cases where users may edit every attribute that you render, using a single ObjectType for both input and output is more convenient. E.g. `BikeType` could be used for both input and output in the `update` action above – if all its fields should be editable.
110
129
 
111
130
  ### Validation
112
131
 
@@ -120,12 +139,59 @@ Taro.config.parse_params = false
120
139
 
121
140
  #### Response validation
122
141
 
123
- Responses are automatically validated to use the correct type for rendering, which guarantees that they match the declaration. This can be disabled:
142
+ Responses are automatically validated to have used the correct type for rendering, which guarantees that they match the declaration.
143
+
144
+ An error is also raised if a documented endpoint renders an undocumented status code, e.g.:
145
+
146
+ ```ruby
147
+ returns code: :ok, type: 'BikeType'
148
+ def create
149
+ render json: BikeType.render(bike), status: :created
150
+ # => undeclared 201 response, raises response validation error
151
+ end
152
+ ```
153
+
154
+ However, all HTTP error codes except 422 can be rendered without prior declaration. E.g.:
155
+
156
+ ```ruby
157
+ returns code: :ok, type: 'BikeType'
158
+ def show
159
+ render json: something, status: :not_found # works
160
+ end
161
+ ```
162
+
163
+ The reason for this is that Rack and Rails commonly render codes like 400, 404, 500 and various others, but you might not want to have that fact documented on every single endpoint.
164
+
165
+ However, if you do declare an error code, responses with this code are validated:
166
+
167
+ ```ruby
168
+ returns code: :not_found, type: 'ExpectedType'
169
+ def show
170
+ render json: WrongType.render(foo), status: :not_found
171
+ # => type mismatch, raises response validation error
172
+ end
173
+ ```
174
+
175
+ Response validation can be disabled:
124
176
 
125
177
  ```ruby
126
178
  Taro.config.validate_responses = false
127
179
  ```
128
180
 
181
+ ### Common error declarations
182
+
183
+ `::common_return` can be used to add a return declaration to all actions in a controller and its subclasses, and all related OpenAPI exports.
184
+
185
+ ```ruby
186
+ class AuthenticatedApiBaseController < ApiBaseController
187
+ common_return code: :unauthorized, type: 'MyErrorType', desc: 'Log in first'
188
+
189
+ rescue_from 'MyAuthError' do
190
+ render json: MyErrorType.render(something), status: :unauthorized
191
+ end
192
+ end
193
+ ```
194
+
129
195
  ### Included type options
130
196
 
131
197
  The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
@@ -170,34 +236,9 @@ end
170
236
 
171
237
  ### FAQ
172
238
 
173
- #### How do I avoid repeating common error declarations?
174
-
175
- Hook into the DSL in your base controller(s):
239
+ #### How do I render API docs?
176
240
 
177
- ```ruby
178
- class ApiBaseController < ApplicationController
179
- def self.api(...)
180
- super
181
- returns code: :not_found, type: 'MyErrorType', desc: 'The record was not found'
182
- end
183
-
184
- rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
185
-
186
- def render_not_found
187
- render json: MyErrorType.render(something), status: :not_found
188
- end
189
- end
190
- ```
191
-
192
- ```ruby
193
- class AuthenticatedApiController < ApiBaseController
194
- def self.api(...)
195
- super
196
- returns code: :unauthorized, type: 'MyErrorType'
197
- end
198
- # ... rescue_from ... render ...
199
- end
200
- ```
241
+ Rendering docs is outside of the scope of this project. You can use the OpenAPI export to generate docs with tools such as RapiDoc, ReDoc, or Swagger UI.
201
242
 
202
243
  #### How do I use context in my types?
203
244
 
@@ -215,16 +256,16 @@ end
215
256
 
216
257
  #### How do I migrate from apipie-rails?
217
258
 
218
- First of all, if you don't need a better OpenAPI export, or better support for hashes and arrays, it might not be worth it.
259
+ First of all, if you don't need a better OpenAPI export, or better support for hashes and arrays, or less repetitive definitions, it might not be worth it.
219
260
 
220
261
  If you do:
221
262
 
222
263
  - note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
223
264
  - extract complex param declarations into InputTypes
224
- - extract complex response declarations into ObjectTypes
265
+ - extract complex response declarations into ObjectTypes or ResponseTypes
225
266
  - replace `required: true` with `null: false` and `required: false` with `null: true`
226
267
 
227
- For a step-by-step migration, you might want to make `taro` use a different DSL then `apipie`:
268
+ Taro uses some of the same DSL as `apipie`, so for a step-by-step migration, you might want to make `taro` use a different one:
228
269
 
229
270
  ```ruby
230
271
  # config/initializers/taro.rb
@@ -294,9 +335,7 @@ end
294
335
  - usage without rails is possible but not convenient yet
295
336
  - rspec matchers for testing
296
337
  - sum types
297
- - api doc rendering based on export (e.g. rails engine with web ui)
298
338
  - [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
299
- - maybe make `type:` optional for path params as they're always strings anyway
300
339
  - various openapi features
301
340
  - non-JSON content types (e.g. for file uploads)
302
341
  - [examples](https://swagger.io/specification/#example-object)
@@ -0,0 +1,31 @@
1
+ # Holds common return definitions for a set of declarations,
2
+ # e.g. shared error responses, within a class and its subclasses.
3
+ module Taro::CommonReturns
4
+ class << self
5
+ def define(klass, nesting = nil, **)
6
+ (map[klass] ||= []) << Taro::ReturnDef.new(nesting:, **)
7
+ klass.extend(InheritedCallback)
8
+ end
9
+
10
+ def inherit(from_class, to_class)
11
+ map[to_class] = map[from_class].dup
12
+ end
13
+
14
+ def for(klass)
15
+ map[klass] || []
16
+ end
17
+
18
+ private
19
+
20
+ def map
21
+ @map ||= {}
22
+ end
23
+ end
24
+
25
+ module InheritedCallback
26
+ def inherited(new_class)
27
+ Taro::CommonReturns.inherit(self, new_class)
28
+ super
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,82 @@
1
+ # Framework-agnostic, abstract class.
2
+ # Descendants must implement #endpoint and (only for openapi export) #routes.
3
+ # See Taro::Rails::Declaration for an example.
4
+ class Taro::Declaration
5
+ attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :tags
6
+
7
+ def initialize(for_klass = nil)
8
+ @params = Class.new(Taro::Types::InputType)
9
+ @return_defs = {}
10
+ @return_descriptions = {}
11
+
12
+ Taro::CommonReturns.for(for_klass).each { |rd| add_return_def(rd) }
13
+ end
14
+
15
+ def add_info(summary, desc: nil, tags: nil)
16
+ summary.is_a?(String) || raise(Taro::ArgumentError, 'api summary must be a String')
17
+ @summary = summary
18
+ @desc = desc
19
+ @tags = Array(tags) if tags
20
+ end
21
+
22
+ def add_param(param_name, **attributes)
23
+ if attributes[:type] == 'Integer'
24
+ attributes[:type] = 'Taro::Types::Scalar::IntegerParamType'
25
+ end
26
+ @params.field(param_name, **attributes)
27
+ end
28
+
29
+ def add_return(nesting = nil, **)
30
+ return_def = Taro::ReturnDef.new(nesting:, **)
31
+ add_return_def(return_def)
32
+ end
33
+
34
+ # Return types are evaluated lazily to avoid unnecessary autoloading
35
+ # of all types in dev/test envs.
36
+ def returns
37
+ @returns ||= evaluate_return_defs
38
+ end
39
+
40
+ def routes
41
+ raise NotImplementedError, "implement ##{__method__} in subclass"
42
+ end
43
+
44
+ def endpoint
45
+ raise NotImplementedError, "implement ##{__method__} in subclass"
46
+ end
47
+
48
+ def polymorphic_route?
49
+ routes.size > 1
50
+ end
51
+
52
+ def inspect
53
+ "#<#{self.class} (#{endpoint || 'not finalized'})>"
54
+ end
55
+
56
+ private
57
+
58
+ def add_return_def(return_def)
59
+ raise_if_already_declared(return_def.code)
60
+
61
+ return_defs[return_def.code] = return_def
62
+ return_descriptions[return_def.code] = return_def.desc
63
+ end
64
+
65
+ def raise_if_already_declared(code)
66
+ (prev = return_defs[code]) && raise(Taro::ArgumentError, <<~MSG)
67
+ response for status #{code} already declared at #{prev.defined_at}
68
+ MSG
69
+ end
70
+
71
+ def evaluate_return_defs
72
+ return_defs.transform_values do |rd|
73
+ type = rd.evaluate
74
+ type.define_name("ResponseType(#{endpoint})") if rd.nesting
75
+ type
76
+ end
77
+ end
78
+
79
+ def <=>(other)
80
+ routes.first.openapi_operation_id <=> other.routes.first.openapi_operation_id
81
+ end
82
+ end
@@ -0,0 +1,34 @@
1
+ module Taro
2
+ def self.declarations
3
+ DeclarationsMap
4
+ end
5
+
6
+ module DeclarationsMap
7
+ class << self
8
+ include Enumerable
9
+
10
+ def [](key)
11
+ map[key]
12
+ end
13
+
14
+ def []=(key, declaration)
15
+ map.key?(key) && raise(Taro::InvariantError, "#{key} already declared")
16
+ map[key] = declaration
17
+ end
18
+
19
+ def each(&)
20
+ map.each_value(&)
21
+ end
22
+
23
+ def reset
24
+ map.clear
25
+ end
26
+
27
+ private
28
+
29
+ def map
30
+ @map ||= {}
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/taro/errors.rb CHANGED
@@ -1,12 +1,25 @@
1
1
  class Taro::Error < StandardError
2
2
  def message
3
3
  # clean up newlines introduced when setting the message with a heredoc
4
- super.chomp.sub(/\n(?=\S)/, ' ')
4
+ super.chomp.tr("\n", ' ')
5
5
  end
6
6
  end
7
7
 
8
8
  class Taro::ArgumentError < Taro::Error; end
9
9
  class Taro::RuntimeError < Taro::Error; end
10
- class Taro::ValidationError < Taro::RuntimeError; end # not to be used directly
10
+ class Taro::InvariantError < Taro::RuntimeError; end
11
+
12
+ class Taro::ValidationError < Taro::RuntimeError
13
+ attr_reader :object, :origin
14
+
15
+ def initialize(message, object, origin)
16
+ raise 'Abstract class' if instance_of?(Taro::ValidationError)
17
+
18
+ super(message)
19
+ @object = object
20
+ @origin = origin
21
+ end
22
+ end
23
+
11
24
  class Taro::InputError < Taro::ValidationError; end
12
25
  class Taro::ResponseError < Taro::ValidationError; end
@@ -1,7 +1,7 @@
1
1
  class Taro::Export::Base
2
2
  attr_reader :result
3
3
 
4
- def self.call(declarations:, title: 'Taro-based API', version: '1.0', **)
4
+ def self.call(declarations: Taro.declarations, title: Taro.config.api_name, version: Taro.config.api_version, **)
5
5
  new.call(declarations:, title:, version:, **)
6
6
  end
7
7
 
@@ -6,8 +6,6 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
6
6
  @schemas = {}
7
7
  end
8
8
 
9
- # TODO:
10
- # - use json-schema gem to validate overall result against OpenAPIv3 schema
11
9
  def call(declarations:, title:, version:)
12
10
  @result = { openapi: '3.1.0', info: { title:, version: } }
13
11
  paths = export_paths(declarations)
@@ -34,7 +32,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
34
32
  operationId: route.openapi_operation_id,
35
33
  parameters: route_parameters(declaration, route),
36
34
  requestBody: request_body(declaration, route),
37
- responses: responses(declaration, route),
35
+ responses: responses(declaration),
38
36
  }.compact,
39
37
  }
40
38
  end
@@ -45,9 +43,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
45
43
 
46
44
  def path_parameters(declaration, route)
47
45
  route.path_params.map do |param_name|
48
- param_field = declaration.params.fields[param_name] || raise(<<~MSG)
49
- Declaration missing for path param #{param_name} of route #{route.endpoint}
50
- MSG
46
+ param_field = declaration.params.fields[param_name] || raise(
47
+ Taro::InvariantError,
48
+ "Declaration missing for path param #{param_name} of route #{route}"
49
+ )
51
50
 
52
51
  # path params are always required in rails
53
52
  export_parameter(param_field).merge(in: 'path', required: true)
@@ -77,8 +76,11 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
77
76
  end
78
77
 
79
78
  def validate_path_or_query_parameter(field)
80
- [:array, :object].include?(field.type.openapi_type) &&
81
- raise("Unexpected object as path/query param #{field.name}: #{field.type}")
79
+ ok = %i[string integer]
80
+ ok.include?(field.type.openapi_type) || raise(Taro::ArgumentError, <<~MSG)
81
+ Unsupported #{field.openapi_type} as path/query param "#{field.name}",
82
+ expected one of: #{ok.join(', ')}
83
+ MSG
82
84
  end
83
85
 
84
86
  def request_body(declaration, route)
@@ -110,28 +112,21 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
110
112
  end
111
113
  end
112
114
 
113
- def responses(declaration, route)
114
- name_anonymous_return_types(declaration, route)
115
-
115
+ def responses(declaration)
116
116
  declaration.returns.sort.to_h do |code, type|
117
+ # response description is required in openapi 3 – fall back to status code
118
+ description = declaration.return_descriptions[code] || type.desc ||
119
+ Taro::StatusCode.coerce_to_message(code)
117
120
  [
118
121
  code.to_s,
119
122
  {
120
- description: declaration.return_descriptions[code],
123
+ description:,
121
124
  content: { 'application/json': { schema: export_type(type) } },
122
125
  }
123
126
  ]
124
127
  end
125
128
  end
126
129
 
127
- def name_anonymous_return_types(declaration, route)
128
- declaration.returns.each do |code, type|
129
- next if type.openapi_name?
130
-
131
- type.openapi_name = "#{route.openapi_operation_id}_#{code}_Response"
132
- end
133
- end
134
-
135
130
  def export_type(type)
136
131
  if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
137
132
  { type: type.openapi_type }
@@ -199,7 +194,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
199
194
  elsif custom_scalar_type?(type)
200
195
  custom_scalar_type_details(type)
201
196
  else
202
- raise NotImplementedError, "Unsupported type: #{type}"
197
+ raise Taro::InvariantError, "Unexpected type: #{type}"
203
198
  end
204
199
  end
205
200
 
@@ -244,8 +239,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
244
239
 
245
240
  def assert_unique_openapi_name(type)
246
241
  @name_to_type_map ||= {}
247
- if (prev = @name_to_type_map[type.openapi_name]) && type != prev
248
- raise("Duplicate openapi_name \"#{type.openapi_name}\" for types #{prev} and #{type}")
242
+ if (prev = @name_to_type_map[type.openapi_name]) && !prev.equivalent?(type)
243
+ raise Taro::InvariantError, <<~MSG
244
+ Duplicate openapi_name "#{type.openapi_name}" for types #{prev} and #{type}
245
+ MSG
249
246
  else
250
247
  @name_to_type_map[type.openapi_name] = type
251
248
  end
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