taro 1.1.0 → 1.3.0

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