taro 1.1.0 → 1.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +76 -9
  4. data/lib/taro/errors.rb +1 -1
  5. data/lib/taro/export/open_api_v3.rb +76 -24
  6. data/lib/taro/rails/declaration.rb +44 -20
  7. data/lib/taro/rails/generators/install_generator.rb +1 -1
  8. data/lib/taro/rails/generators/templates/errors_type.erb +15 -10
  9. data/lib/taro/rails/normalized_route.rb +8 -0
  10. data/lib/taro/rails/tasks/export.rake +5 -1
  11. data/lib/taro/types/base_type.rb +2 -0
  12. data/lib/taro/types/coercion.rb +24 -14
  13. data/lib/taro/types/field.rb +8 -16
  14. data/lib/taro/types/field_validation.rb +1 -1
  15. data/lib/taro/types/list_type.rb +4 -6
  16. data/lib/taro/types/object_types/free_form_type.rb +1 -0
  17. data/lib/taro/types/object_types/no_content_type.rb +1 -0
  18. data/lib/taro/types/object_types/page_info_type.rb +2 -0
  19. data/lib/taro/types/object_types/page_type.rb +15 -25
  20. data/lib/taro/types/scalar/iso8601_date_type.rb +3 -3
  21. data/lib/taro/types/scalar/iso8601_datetime_type.rb +3 -3
  22. data/lib/taro/types/scalar/string_type.rb +17 -6
  23. data/lib/taro/types/scalar/timestamp_type.rb +1 -0
  24. data/lib/taro/types/scalar/uuid_v4_type.rb +3 -20
  25. data/lib/taro/types/scalar_type.rb +1 -0
  26. data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
  27. data/lib/taro/types/shared/deprecation.rb +3 -0
  28. data/lib/taro/types/shared/derived_types.rb +27 -0
  29. data/lib/taro/types/shared/errors.rb +3 -1
  30. data/lib/taro/types/shared/fields.rb +6 -5
  31. data/lib/taro/types/shared/item_type.rb +1 -0
  32. data/lib/taro/types/shared/object_coercion.rb +13 -0
  33. data/lib/taro/types/shared/openapi_name.rb +8 -6
  34. data/lib/taro/types/shared/pattern.rb +69 -0
  35. data/lib/taro/types/shared/rendering.rb +4 -4
  36. data/lib/taro/version.rb +1 -1
  37. data/tasks/benchmark.rake +1 -1
  38. metadata +7 -5
  39. data/lib/taro/types/shared/derivable_types.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36e27c067e70b65d503a3c78b61e9efc05d8995f9927edfaf6f66ff4fae70b25
4
- data.tar.gz: 131fc3d1bea432320046c4e5c220dff0a3ead87d8a2f3d12310cdd847b2e50c1
3
+ metadata.gz: 41f14035882a39e03a7bae9c799a42c9fa63414e4201fbbab7f876d54d8d214b
4
+ data.tar.gz: 768b505506f8ad58792d0d86b6152f1aa60da98419aef111b9fc187ee74eab42
5
5
  SHA512:
6
- metadata.gz: 9d44a2c33be8175c6b59b1426921fd339c4e4125bf7fa076da27f4b9c17bcf1efb69aa0233c8e91f16804b086f250b37fddb0e6e6dce2debbf0349fc96ac832d
7
- data.tar.gz: 59d1a63e68b5560ee7cf0243ff3ccf6684376249745727e6695576821285e8dc1d64018a5f8e8a962f9d7dcea74c1e6bc9d27dd664662d327c257d81505b6c23
6
+ metadata.gz: 27607c95ed9c5a7ab2c4d579dec41357657f24c694f43eac06b15677a82e491fd5986c5cad8b7bf999ee23ee456d75cc2c90debee47e93d02cdc901b4e45f498
7
+ data.tar.gz: f99bfdfe66f7a09ac1746c23e2671bcec7f20cfee23e980db76561f14e1ea082bc2fcc59e4650af8732d7e608a8b7c012473dc0f862bb15ff9d89501a641c825
data/CHANGELOG.md CHANGED
@@ -1,8 +1,47 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2024-11-25
4
+
5
+ ### Added
6
+
7
+ - Support for string patterns (on StringType children and in export)
8
+
9
+ ### Fixed
10
+
11
+ - Fixed OpenAPI export of params with enum (inline & type-based)
12
+
13
+ ## [1.2.0] - 2024-11-18
14
+
15
+ ### Added
16
+
17
+ - Improved error messages
18
+ - Option to define custom derived types
19
+ - Option to use custom keys in paginated content
20
+ - Option to deprecate individual fields, params, and types
21
+
22
+ ### Fixed
23
+
24
+ - Fixed nullable enum fields raising for null input
25
+ - Fixed auto-loading of return types
26
+ - Fixed console spam when inspecting declarations
27
+ - Fixed resolver method not being used when rendering a Hash
28
+ - Fixed the ErrorsType template
29
+ - Many fixes for OpenAPI export
30
+ - Fixed export of parameters for http methods without body
31
+ - Fixed export for PageType
32
+ - Fixed export for arrays of UUIDs, Dates, and Times
33
+ - Fixed export YML keys for namespaced controllers
34
+ - Reference plain types for repeated flat return types
35
+ - Made order of paths, verbs, responses and schemas deterministic
36
+
3
37
  ## [1.1.0] - 2024-11-16
4
38
 
39
+ ### Added
40
+
5
41
  - Response validation refined
42
+
43
+ ### Fixed
44
+
6
45
  - Bugfix for openapi export
7
46
 
8
47
  ## [1.0.0] - 2024-11-14
data/README.md CHANGED
@@ -131,16 +131,18 @@ Taro.config.validate_responses = false
131
131
  The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
132
132
 
133
133
  - `'Boolean'` - accepts and renders `true` or `false`
134
+ - `'Date'` - accepts and renders a date string in ISO8601 format
135
+ - `'DateTime'` - an alias for `'Time'`
134
136
  - `'Float'`
135
137
  - `'FreeForm'` - accepts and renders any JSON-serializable object, use with care
136
138
  - `'Integer'`
137
139
  - `'NoContent'` - renders an empty object, for use with `status: :no_content`
138
140
  - `'String'`
141
+ - `'Time'` - accepts and renders a time string in ISO8601 format
139
142
  - `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
140
143
  - `'UUID'` - accepts and renders UUIDs
141
- - `'Date'` - accepts and renders a date string in ISO8601 format
142
- - `'Time'` - accepts and renders a time string in ISO8601 format
143
- - `'DateTime'` - an alias for `'Time'`
144
+
145
+ Also, when using the generator, `ErrorsType` and `ErrorDetailsType` are generated as a starting point for unified error presentation. `ErrorsType` can render invalid `ActiveRecord` instances, `ActiveModel::Errors` and other data structures.
144
146
 
145
147
  ### Enums
146
148
 
@@ -168,7 +170,7 @@ end
168
170
 
169
171
  ### FAQ
170
172
 
171
- #### How to avoid repeating common error declarations?
173
+ #### How do I avoid repeating common error declarations?
172
174
 
173
175
  Hook into the DSL in your base controller(s):
174
176
 
@@ -197,7 +199,7 @@ class AuthenticatedApiController < ApiBaseController
197
199
  end
198
200
  ```
199
201
 
200
- #### How to use context in my types?
202
+ #### How do I use context in my types?
201
203
 
202
204
  Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
203
205
 
@@ -211,13 +213,80 @@ class BikeType < ObjectType
211
213
  end
212
214
  ```
213
215
 
216
+ #### How do I migrate from apipie-rails?
217
+
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.
219
+
220
+ If you do:
221
+
222
+ - note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
223
+ - extract complex param declarations into InputTypes
224
+ - extract complex response declarations into ObjectTypes
225
+ - replace `required: true` with `null: false` and `required: false` with `null: true`
226
+
227
+ For a step-by-step migration, you might want to make `taro` use a different DSL then `apipie`:
228
+
229
+ ```ruby
230
+ # config/initializers/taro.rb
231
+ %i[api param returns].each do |m|
232
+ Taro::Rails::DSL.alias_method("taro_#{m}", m) # `taro_api` etc.
233
+ Taro::Rails::DSL.define_method(m) { |*a, **k, &b| super(*a, **k, &b) }
234
+ end
235
+ ```
236
+
237
+ #### How do I keep lengthy API descriptions out of my controller?
238
+
239
+ ```ruby
240
+ module BikeUpdateDesc
241
+ extend ActiveSupport::Concern
242
+
243
+ included do
244
+ api 'Update a bike', description: 'Long description', tags: ['Bikes']
245
+ # lots of params and returns ...
246
+ end
247
+ end
248
+
249
+ class BikesController < ApplicationController
250
+ include BikeUpdateDesc
251
+ def update # ...
252
+ end
253
+ ```
254
+
214
255
  #### Why do I have to use type name strings instead of the type constants?
215
256
 
216
257
  Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
217
258
 
218
259
  The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
219
260
 
220
- This already works fo type classes they don't trigger loading of referenced types unless used. The API declarations in controller classes still trigger auto-loading for now, but we aim to improve this in the future.
261
+ #### Can I define my own derived types like `page_of` or `array_of`?
262
+
263
+ Yes.
264
+
265
+ ```ruby
266
+ # Implement ::derive_from in your custom type.
267
+ class PreviewType < Taro::Types::Scalar::StringType
268
+ singleton_class.attr_reader :type_to_preview
269
+
270
+ def self.derive_from(other_type)
271
+ self.type_to_preview = other_type
272
+ end
273
+
274
+ def coerce_response
275
+ type_to_preview.new(object).coerce_response.to_s.truncate(100)
276
+ end
277
+ end
278
+
279
+ # Make it available in the DSL, e.g. in an initializer.
280
+ Taro::Types::BaseType.define_derived_type :preview, 'PreviewType'
281
+
282
+ # Usage:
283
+ class MyController < ApplicationController
284
+ returns code: :ok, preview_of: 'BikeType'
285
+ def show
286
+ render json: BikeType.preview.render(Bike.find(params[:id]))
287
+ end
288
+ end
289
+ ```
221
290
 
222
291
  ## Possible future features
223
292
 
@@ -227,7 +296,6 @@ This already works fo type classes – they don't trigger loading of referenced
227
296
  - sum types
228
297
  - api doc rendering based on export (e.g. rails engine with web ui)
229
298
  - [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
230
- - deprecation feature
231
299
  - maybe make `type:` optional for path params as they're always strings anyway
232
300
  - various openapi features
233
301
  - non-JSON content types (e.g. for file uploads)
@@ -237,8 +305,7 @@ This already works fo type classes – they don't trigger loading of referenced
237
305
  - mixed enums
238
306
  - nullable enums
239
307
  - string format specifications (e.g. binary, int64, password ...)
240
- - string pattern specifications
241
- - string minLength and maxLength
308
+ - string minLength and maxLength (substitute: `self.pattern = /\A.{3,5}\z/`)
242
309
  - number minimum, exclusiveMinimum, maximum, multipleOf
243
310
  - readOnly, writeOnly
244
311
 
data/lib/taro/errors.rb CHANGED
@@ -1,7 +1,7 @@
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.tr("\n", ' ')
4
+ super.chomp.sub(/\n(?=\S)/, ' ')
5
5
  end
6
6
  end
7
7
 
@@ -11,13 +11,13 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
11
11
  def call(declarations:, title:, version:)
12
12
  @result = { openapi: '3.1.0', info: { title:, version: } }
13
13
  paths = export_paths(declarations)
14
- @result[:paths] = paths if paths.any?
15
- @result[:components] = { schemas: } if schemas.any?
14
+ @result[:paths] = paths.sort.to_h if paths.any?
15
+ @result[:components] = { schemas: schemas.sort.to_h } if schemas.any?
16
16
  self
17
17
  end
18
18
 
19
19
  def export_paths(declarations)
20
- declarations.each_with_object({}) do |declaration, paths|
20
+ declarations.sort.each_with_object({}) do |declaration, paths|
21
21
  declaration.routes.each do |route|
22
22
  paths[route.openapi_path] ||= {}
23
23
  paths[route.openapi_path].merge! export_route(route, declaration)
@@ -31,30 +31,58 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
31
31
  description: declaration.desc,
32
32
  summary: declaration.summary,
33
33
  tags: declaration.tags,
34
- parameters: path_parameters(declaration, route),
34
+ parameters: route_parameters(declaration, route),
35
35
  requestBody: request_body(declaration, route),
36
36
  responses: responses(declaration),
37
37
  }.compact,
38
38
  }
39
39
  end
40
40
 
41
+ def route_parameters(declaration, route)
42
+ path_parameters(declaration, route) + query_parameters(declaration, route)
43
+ end
44
+
41
45
  def path_parameters(declaration, route)
42
46
  route.path_params.map do |param_name|
43
47
  param_field = declaration.params.fields[param_name] || raise(<<~MSG)
44
48
  Declaration missing for path param #{param_name} of route #{route.endpoint}
45
49
  MSG
46
50
 
47
- {
48
- name: param_field.name,
49
- in: 'path',
50
- description: param_field.desc,
51
- required: true, # path params are always required in rails
52
- schema: { type: param_field.openapi_type },
53
- }.compact
51
+ # path params are always required in rails
52
+ export_parameter(param_field).merge(in: 'path', required: true)
53
+ end
54
+ end
55
+
56
+ def query_parameters(declaration, route)
57
+ return [] if route.can_have_request_body?
58
+
59
+ declaration.params.fields.filter_map do |name, param_field|
60
+ next if route.path_params.include?(name)
61
+
62
+ export_parameter(param_field).merge(in: 'query')
54
63
  end
55
64
  end
56
65
 
66
+ def export_parameter(field)
67
+ validate_path_or_query_parameter(field)
68
+
69
+ {
70
+ name: field.name,
71
+ deprecated: field.deprecated,
72
+ description: field.desc,
73
+ required: !field.null,
74
+ schema: export_field(field).except(:deprecated, :description),
75
+ }.compact
76
+ end
77
+
78
+ def validate_path_or_query_parameter(field)
79
+ [:array, :object].include?(field.type.openapi_type) &&
80
+ raise("Unexpected object as path/query param #{field.name}: #{field.type}")
81
+ end
82
+
57
83
  def request_body(declaration, route)
84
+ return unless route.can_have_request_body?
85
+
58
86
  params = declaration.params
59
87
  body_param_fields = params.fields.reject do |name, _field|
60
88
  route.path_params.include?(name)
@@ -66,7 +94,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
66
94
  body_input_type.openapi_name = params.openapi_name
67
95
 
68
96
  # For polymorphic routes (more than one for the same declaration),
69
- # we can't use refs because they request body might differ.
97
+ # we can't use refs because their request body might differ:
70
98
  # Different params might be in the path vs. in the request body.
71
99
  use_refs = !declaration.polymorphic_route?
72
100
  schema = request_body_schema(body_input_type, use_refs:)
@@ -82,7 +110,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
82
110
  end
83
111
 
84
112
  def responses(declaration)
85
- declaration.returns.to_h do |code, type|
113
+ declaration.returns.sort.to_h do |code, type|
86
114
  [
87
115
  code.to_s,
88
116
  {
@@ -94,13 +122,17 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
94
122
  end
95
123
 
96
124
  def export_type(type)
97
- if type < Taro::Types::ScalarType
125
+ if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
98
126
  { type: type.openapi_type }
99
127
  else
100
128
  extract_component_ref(type)
101
129
  end
102
130
  end
103
131
 
132
+ def custom_scalar_type?(type)
133
+ type < Taro::Types::ScalarType && (type.openapi_pattern || type.deprecated)
134
+ end
135
+
104
136
  def export_field(field)
105
137
  if field.type < Taro::Types::ScalarType
106
138
  export_scalar_field(field)
@@ -115,23 +147,29 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
115
147
  # as it puts props like format together with the main type.
116
148
  # https://github.com/OAI/OpenAPI-Specification/issues/3148
117
149
  base = { oneOf: [base, { type: 'null' }] } if field.null
118
- base[:description] = field.desc if field.desc
119
- base[:default] = field.default if field.default_specified?
120
- base[:enum] = field.enum if field.enum
121
- base
150
+ base.merge(field_metadata(field))
122
151
  end
123
152
 
124
153
  def export_complex_field_ref(field)
125
154
  ref = extract_component_ref(field.type)
155
+ return ref if field_metadata(field).empty? && !field.null
156
+
126
157
  if field.null
127
158
  # RE nullable: https://stackoverflow.com/a/70658334
128
- { description: field.desc, oneOf: [ref, { type: 'null' }] }.compact
129
- elsif field.desc
159
+ { oneOf: [ref, { type: 'null' }] }
160
+ else # i.e. with metadata such as description or deprecated
130
161
  # https://github.com/OAI/OpenAPI-Specification/issues/2033
131
- { description: field.desc, allOf: [ref] }
132
- else
133
- ref
134
- end
162
+ { allOf: [ref] }
163
+ end.merge(field_metadata(field))
164
+ end
165
+
166
+ def field_metadata(field)
167
+ meta = {}
168
+ meta[:description] = field.desc if field.desc
169
+ meta[:deprecated] = field.deprecated unless field.deprecated.nil?
170
+ meta[:default] = field.default if field.default_specified?
171
+ meta[:enum] = field.enum if field.enum
172
+ meta
135
173
  end
136
174
 
137
175
  def extract_component_ref(type)
@@ -147,6 +185,8 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
147
185
  enum_type_details(type)
148
186
  elsif type < Taro::Types::ListType
149
187
  list_type_details(type)
188
+ elsif custom_scalar_type?(type)
189
+ custom_scalar_type_details(type)
150
190
  else
151
191
  raise NotImplementedError, "Unsupported type: #{type}"
152
192
  end
@@ -156,6 +196,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
156
196
  required = type.fields.values.reject(&:null).map(&:name)
157
197
  {
158
198
  type: type.openapi_type,
199
+ deprecated: type.deprecated,
159
200
  description: type.desc,
160
201
  required: (required if required.any?),
161
202
  properties: type.fields.to_h { |name, f| [name, export_field(f)] },
@@ -166,6 +207,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
166
207
  def enum_type_details(enum)
167
208
  {
168
209
  type: enum.item_type.openapi_type,
210
+ deprecated: enum.deprecated,
169
211
  description: enum.desc,
170
212
  enum: enum.values,
171
213
  }.compact
@@ -174,11 +216,21 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
174
216
  def list_type_details(list)
175
217
  {
176
218
  type: 'array',
219
+ deprecated: list.deprecated,
177
220
  description: list.desc,
178
221
  items: export_type(list.item_type),
179
222
  }.compact
180
223
  end
181
224
 
225
+ def custom_scalar_type_details(scalar)
226
+ {
227
+ type: scalar.openapi_type,
228
+ deprecated: scalar.deprecated,
229
+ description: scalar.desc,
230
+ pattern: scalar.openapi_pattern,
231
+ }.compact
232
+ end
233
+
182
234
  def assert_unique_openapi_name(type)
183
235
  @name_to_type_map ||= {}
184
236
  if (prev = @name_to_type_map[type.openapi_name]) && type != prev
@@ -1,9 +1,9 @@
1
1
  class Taro::Rails::Declaration
2
- attr_reader :desc, :summary, :params, :returns, :return_descriptions, :return_nestings, :routes, :tags
2
+ attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :return_nestings, :routes, :tags
3
3
 
4
4
  def initialize
5
5
  @params = Class.new(Taro::Types::InputType)
6
- @returns = {}
6
+ @return_defs = {}
7
7
  @return_descriptions = {}
8
8
  @return_nestings = {}
9
9
  end
@@ -24,8 +24,11 @@ class Taro::Rails::Declaration
24
24
  status = self.class.coerce_status_to_int(code)
25
25
  raise_if_already_declared(status)
26
26
 
27
+ kwargs[:nesting] = nesting
28
+ check_return_kwargs(kwargs)
29
+
27
30
  kwargs[:defined_at] = caller_locations(1..2)[1]
28
- returns[status] = return_type_from(nesting, **kwargs)
31
+ return_defs[status] = kwargs
29
32
 
30
33
  # response desc is required in openapi 3 – fall back to status code
31
34
  return_descriptions[status] = desc || code.to_s
@@ -34,14 +37,19 @@ class Taro::Rails::Declaration
34
37
  return_nestings[status] = nesting if nesting
35
38
  end
36
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
+
37
46
  def raise_if_already_declared(status)
38
- returns[status] &&
47
+ return_defs[status] &&
39
48
  raise(Taro::ArgumentError, "response for status #{status} already declared")
40
49
  end
41
50
 
42
51
  def parse_params(rails_params)
43
- hash = params.new(rails_params.to_unsafe_h).coerce_input
44
- hash
52
+ params.new(rails_params.to_unsafe_h).coerce_input
45
53
  end
46
54
 
47
55
  def finalize(controller_class:, action_name:)
@@ -67,10 +75,15 @@ class Taro::Rails::Declaration
67
75
  # TODO: these change when the controller class is renamed.
68
76
  # We might need a way to set `base`. Perhaps as a kwarg to `::api`?
69
77
  def add_openapi_names(controller_class:, action_name:)
70
- base = "#{controller_class.name.chomp('Controller').sub('::', '_')}_#{action_name}"
78
+ base = "#{controller_class.name.chomp('Controller').gsub('::', '_')}_#{action_name}"
71
79
  params.openapi_name = "#{base}_Input"
80
+ params.define_singleton_method(:name) { openapi_name }
81
+
72
82
  returns.each do |status, return_type|
83
+ next if return_type.openapi_name? # only set for ad-hoc / nested return types
84
+
73
85
  return_type.openapi_name = "#{base}_#{status}_Response"
86
+ return_type.define_singleton_method(:name) { openapi_name }
74
87
  end
75
88
  end
76
89
 
@@ -86,19 +99,11 @@ class Taro::Rails::Declaration
86
99
 
87
100
  private
88
101
 
89
- def return_type_from(nesting, **kwargs)
90
- if nesting
91
- # ad-hoc return type, requiring the actual return type to be nested
92
- Class.new(Taro::Types::ObjectType).tap do |type|
93
- type.field(nesting, null: false, **kwargs)
94
- end
95
- else
96
- check_return_kwargs(kwargs)
97
- Taro::Types::Coercion.call(kwargs)
98
- end
99
- end
100
-
101
102
  def check_return_kwargs(kwargs)
103
+ # For nested returns, evaluate_return_def calls ::field, which validates
104
+ # field options, but does not trigger type autoloading.
105
+ return evaluate_return_def(**kwargs) if kwargs[:nesting]
106
+
102
107
  if kwargs.key?(:null)
103
108
  raise Taro::ArgumentError, <<~MSG
104
109
  `null:` is not supported for top-level returns. If you want a nullable return
@@ -106,13 +111,32 @@ class Taro::Rails::Declaration
106
111
  MSG
107
112
  end
108
113
 
109
- bad_keys = kwargs.keys - (Taro::Types::Coercion::KEYS + %i[code defined_at desc])
114
+ bad_keys = kwargs.keys - (Taro::Types::Coercion.keys + %i[code desc nesting])
110
115
  return if bad_keys.empty?
111
116
 
112
117
  raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
113
118
  end
114
119
 
120
+ def evaluate_return_defs
121
+ return_defs.transform_values { |defi| evaluate_return_def(**defi) }
122
+ end
123
+
124
+ def evaluate_return_def(nesting:, **kwargs)
125
+ if nesting
126
+ # ad-hoc return type, requiring the actual return type to be nested
127
+ Class.new(Taro::Types::ObjectType).tap do |type|
128
+ type.field(nesting, null: false, **kwargs)
129
+ end
130
+ else
131
+ Taro::Types::Coercion.call(kwargs)
132
+ end
133
+ end
134
+
115
135
  def raise_missing_route(controller_class, action_name)
116
136
  raise(Taro::ArgumentError, "No route found for #{controller_class}##{action_name}")
117
137
  end
138
+
139
+ def <=>(other)
140
+ params.openapi_name <=> other.params.openapi_name
141
+ end
118
142
  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
- copy_file tmpl, "#{dest_dir}/#{File.basename(tmpl).sub('erb', 'rb')}"
15
+ template tmpl, "#{dest_dir}/#{File.basename(tmpl).sub('erb', 'rb')}"
16
16
  end
17
17
  end
18
18
  # :nocov:
@@ -11,15 +11,20 @@ class ErrorsType < Taro::Types::ListType
11
11
  end
12
12
 
13
13
  def coerce_response
14
- case object.class.name
15
- when 'ActiveRecord::Base'
16
- super(object.errors.errors)
17
- when 'ActiveModel::Errors'
18
- super(object.errors)
19
- when 'Hash', 'Interactor::Context'
20
- super(object[:errors])
21
- else # e.g. Array
22
- super(object)
23
- end
14
+ list =
15
+ case object<%- if defined?(ActiveRecord) %>
16
+ when ActiveModel::Errors
17
+ object.errors
18
+ when ActiveRecord::Base
19
+ object.errors.errors<%- end %>
20
+ when Array
21
+ object
22
+ when Hash<%- if defined?(Interactor::Context) %>, Interactor::Context<%- end %>
23
+ object.to_h.fetch(:errors)
24
+ else
25
+ response_error("must be an Enumerable or an object with errors")
26
+ end
27
+
28
+ list.map { |el| self.class.item_type.new(el).coerce_response }
24
29
  end
25
30
  end
@@ -26,4 +26,12 @@ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
26
26
  controller, action = rails_route.requirements.values_at(:controller, :action)
27
27
  "#{controller}##{action}"
28
28
  end
29
+
30
+ def can_have_request_body?
31
+ %w[patch post put].include?(verb)
32
+ end
33
+
34
+ def inspect
35
+ %(#<#{self.class} "#{verb} #{openapi_path}">)
36
+ end
29
37
  end
@@ -10,6 +10,10 @@ task 'taro:export' => :environment do
10
10
  version: Taro.config.api_version,
11
11
  )
12
12
 
13
- data = export.result.send("to_#{Taro.config.export_format}")
13
+ data = export.send("to_#{Taro.config.export_format}")
14
+
15
+ FileUtils.mkdir_p(File.dirname(Taro.config.export_path))
14
16
  File.write(Taro.config.export_path, data)
17
+
18
+ puts "Exported #{Taro.config.api_name} to #{Taro.config.export_path}"
15
19
  end
@@ -9,6 +9,8 @@
9
9
  Taro::Types::BaseType = Data.define(:object) do
10
10
  require_relative "shared"
11
11
  extend Taro::Types::Shared::AdditionalProperties
12
+ extend Taro::Types::Shared::Deprecation
13
+ extend Taro::Types::Shared::DerivedTypes
12
14
  extend Taro::Types::Shared::Description
13
15
  extend Taro::Types::Shared::OpenAPIName
14
16
  extend Taro::Types::Shared::OpenAPIType
@@ -1,42 +1,52 @@
1
1
  module Taro::Types::Coercion
2
- KEYS = %i[type array_of page_of].freeze
3
-
4
2
  class << self
5
3
  def call(arg)
6
4
  validate_hash(arg)
7
5
  from_hash(arg)
8
6
  end
9
7
 
8
+ # Coercion keys can be expanded by the DerivedTypes module.
9
+ def keys
10
+ @keys ||= %i[type]
11
+ end
12
+
13
+ def derived_suffix
14
+ '_of'
15
+ end
16
+
10
17
  private
11
18
 
12
19
  def validate_hash(arg)
13
20
  arg.is_a?(Hash) || raise(Taro::ArgumentError, <<~MSG)
14
- Type coercion argument must be a Hash, got: #{arg.inspect} (#{arg.class})
21
+ Type coercion argument must be a Hash, got: #{arg.class}
15
22
  MSG
16
23
 
17
- types = arg.slice(*KEYS)
24
+ types = arg.slice(*keys)
18
25
  types.size == 1 || raise(Taro::ArgumentError, <<~MSG)
19
- Exactly one of type, array_of, or page_of must be given, got: #{types}
26
+ Exactly one of #{keys.join(', ')} must be given, got: #{types}
20
27
  MSG
21
28
  end
22
29
 
23
30
  def from_hash(hash)
24
- if hash[:type]
25
- from_string(hash[:type])
26
- elsif (inner_type = hash[:array_of])
27
- from_string(inner_type).array
28
- elsif (inner_type = hash[:page_of])
29
- from_string(inner_type).page
30
- else
31
- raise NotImplementedError, 'Unsupported type coercion'
31
+ keys.each do |key|
32
+ next unless (value = hash[key])
33
+
34
+ # e.g. `returns type: 'MyType'` -> MyType
35
+ return from_string(value) if key == :type
36
+
37
+ # DerivedTypes
38
+ # e.g. `returns array_of: 'MyType'` -> MyType.array
39
+ return from_string(value).send(key.to_s.chomp(derived_suffix))
32
40
  end
41
+
42
+ raise NotImplementedError, "Unsupported type coercion #{hash}"
33
43
  end
34
44
 
35
45
  def from_string(arg)
36
46
  shortcuts[arg] || from_class(Object.const_get(arg.to_s))
37
47
  rescue NameError
38
48
  raise Taro::ArgumentError, <<~MSG
39
- Unsupported type: #{arg}. It should be a type-class name
49
+ No such type: #{arg}. It should be a type-class name
40
50
  or one of #{shortcuts.keys.map(&:inspect).join(', ')}.
41
51
  MSG
42
52
  end