action_spec 1.3.0 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02c55ca1816d1b61ad17a4a735c1704bb887cc4f67d842a7ed351641c47e93d8
4
- data.tar.gz: a148b5f034a92b4f6648c3deb5e29aa997588e6162cd761e3586e9840d6b0fa2
3
+ metadata.gz: 9218555ca7d33709a7a5b22528f6900358e36247c123e521f2342d7a36ac02b5
4
+ data.tar.gz: e771af58669e707ccbcc922b6d9afa726a2b25817ab4836e01ce1154bd22344b
5
5
  SHA512:
6
- metadata.gz: 1eb2aa363f52d5800497364315168186b3fb04fc0fc1541ebe2075a73284585a5302b68f48183f5163451607f53bddc42474a988a6549dcb4daf7849bd2bf9aa
7
- data.tar.gz: 985e5bd7551b4f6bd9919209137c90cae86a3a2720113dacb01e421849fca4e16a602550fb833a920d703f997c97a2b8b87c49ceda5ce4ecefa9bfea1a641988
6
+ metadata.gz: 7d03ee8f19de7c1c874302344eadf224f9aa3fba2dab228f2f4b44448a2865ba53dd97385587b6b9691308e4a987048b1eab6e5f55a1b03ef93424a643857a0c
7
+ data.tar.gz: 357fe37dc9a591740079c76dd3e4238a9485cb0d1e57ab020bed09bf737d1937ef22b777f8b062f78473fe497965c2df754ee3858a97f77bdd59674002cdda69
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # ActionSpec [WIP]
1
+ # ActionSpec
2
2
 
3
- Concise and Powerful API Documentation Solution for Rails.
3
+ Concise and Powerful API Documentation Solution for Rails. [中文](README_zh.md)
4
4
 
5
5
  <img src=".github/assets/action_spec.jpg" />
6
6
 
@@ -10,27 +10,31 @@ Concise and Powerful API Documentation Solution for Rails.
10
10
 
11
11
  ## Table Of Contents
12
12
 
13
- - [OpenAPI Generation](#openapi-generation)
14
- - [Doc DSL](#doc-dsl)
15
- - [`doc`](#doc)
16
- - [`doc_dry`](#doc_dry)
17
- - [`openapi false`](#openapi-false)
18
- - [`tag`](#tag)
19
- - [DSL Reference](#dsl-reference)
20
- - [Schemas](#schemas)
21
- - [Declare A Required Field](#declare-a-required-field)
22
- - [Field Types](#field-types)
23
- - [Field Options](#field-options)
24
- - [Schemas From ActiveRecord](#schemas-from-activerecord)
25
- - [Type And Boundary Matrix](#type-and-boundary-matrix)
26
- - [Parameter Validation And Type Coercion](#parameter-validation-and-type-coercion)
27
- - [Validation Flow](#validation-flow)
28
- - [Reading Processed Values With `px`](#reading-processed-values-with-px)
29
- - [Errors](#errors)
30
- - [Configuration And I18n](#configuration-and-i18n)
31
- - [Configuration](#configuration)
32
- - [I18n](#i18n)
33
- - [AI Generation Style Guide](#ai-generation-style-guide)
13
+ 1. [OpenAPI Generation](#openapi-generation)
14
+ 2. [Doc DSL](#doc-dsl)
15
+ 1. [`doc`](#doc)
16
+ 2. [`doc_dry`](#doc_dry)
17
+ 3. [DSL Inside `doc`](#dsl-inside-doc)
18
+ 1. [Parameter](#parameter)
19
+ 2. [Request body](#request-body)
20
+ 3. [`openapi false`](#openapi-false)
21
+ 4. [Scope](#scope)
22
+ 5. [Response](#response)
23
+ 3. [Schemas](#schemas)
24
+ 1. [Declare A Required Field](#declare-a-required-field)
25
+ 2. [Field Types](#field-types)
26
+ 3. [Field Options](#field-options)
27
+ 4. [Schemas From ActiveRecord](#schemas-from-activerecord)
28
+ 5. [Type And Boundary Matrix](#type-and-boundary-matrix)
29
+ 4. [Parameter Validation And Type Coercion](#parameter-validation-and-type-coercion)
30
+ 1. [Validation Flow](#validation-flow)
31
+ 2. [Reading Processed Values With `px`](#reading-processed-values-with-px)
32
+ 3. [`px` is Whitelist Extraction](#px-is-whitelist-extraction)
33
+ 4. [Errors](#errors)
34
+ 5. [Configuration And I18n](#configuration-and-i18n)
35
+ 1. [Configuration](#configuration)
36
+ 2. [I18n](#i18n)
37
+ 6. [AI Generation Style Guide](#ai-generation-style-guide)
34
38
 
35
39
  ## Example
36
40
 
@@ -41,28 +45,27 @@ class UsersController < ApplicationController
41
45
  doc {
42
46
  header :Authorization, String
43
47
  path :account_id, Integer
44
- query :locale, String, default: "zh-CN"
45
- query :page, Integer, default: -> { 1 }
48
+ query :locale, String, default: "zh-CN", transform: -> { it.downcase }
49
+ query :key_number, Integer, default: -> { it + 1 }, px: :key
46
50
 
47
51
  form data: {
48
- name!: String,
49
- age: Integer,
52
+ name!: { type: String, transform: :strip },
50
53
  birthday: Date,
51
54
  admin: { type: :boolean, default: false },
52
- tags: [String],
53
- profile: {
54
- nickname!: String
55
- }
55
+ tags: [{ id: Integer, content!: { type: String, blank: false } }],
56
+ profile: { nickname!: String }
56
57
  }
57
58
 
58
- response 200, desc: "success"
59
+ response 200, "success", :json, data: { code: Integer, result: [Hash] }
60
+ errors 400, "client errors", {
61
+ invalid_params: { code: 1234, message: "parameters are invalid" },
62
+ missing_params: { code: 1235, message: "parameters are missing" }
63
+ }
59
64
  }
60
65
  def create
61
- User.create!(
62
- account_id: px[:account_id],
63
- name: px[:name],
64
- birthday: px[:birthday],
65
- admin: px[:admin]
66
+ User.find(px[:account_id]).update!(
67
+ key: px[:key], name: px[:name],
68
+ **px.slice(:birthday, :admin, :profile)
66
69
  )
67
70
  end
68
71
  end
@@ -95,7 +98,7 @@ By default, this writes to:
95
98
  docs/openapi.yml
96
99
  ```
97
100
 
98
- For one-off runs, environment variables can override the default output path and document metadata:
101
+ Environment variables can override the default output path and document metadata:
99
102
 
100
103
  ```bash
101
104
  bin/rails action_spec:gen \
@@ -149,6 +152,18 @@ def create
149
152
  end
150
153
  ```
151
154
 
155
+ Override the default OpenAPI tag with `tag:`. By default, the tag comes from the routed `controller_path`:
156
+
157
+ ```ruby
158
+ doc_dry(:index, tag: "backoffice")
159
+
160
+ doc("List users", tag: "members") {
161
+ query :status, String
162
+ }
163
+ ```
164
+
165
+ Generated OpenAPI operations also include an `operationId`, built from the final tag plus the action name, for example `members_index` or `users_create`.
166
+
152
167
  ### `doc_dry`
153
168
 
154
169
  ```ruby
@@ -166,31 +181,7 @@ end
166
181
 
167
182
  All matching dry blocks are applied before the action-specific `doc`.
168
183
 
169
- ### `openapi false`
170
-
171
- You can also opt an action out of OpenAPI generation from either `doc` or `doc_dry`:
172
-
173
- ```ruby
174
- doc {
175
- openapi false
176
- }
177
- ```
178
-
179
- ### `tag`
180
-
181
- OpenAPI tags can also be set at either level:
182
-
183
- ```ruby
184
- doc_dry(:index, tag: "backoffice")
185
-
186
- doc("List users", tag: "members") {
187
- query :status, String
188
- }
189
- ```
190
-
191
- Generated OpenAPI operations also include an `operationId`, built from the final tag plus the action name, for example `members_index` or `users_create`.
192
-
193
- ### DSL Reference
184
+ ### DSL Inside `doc`
194
185
 
195
186
  #### Parameter
196
187
 
@@ -208,7 +199,16 @@ cookie :remember_token, String
208
199
  cookie! :remember_token, String
209
200
  ```
210
201
 
211
- Bang methods mark the field as required. For example, `query! :page, Integer` means the request must include `page`.
202
+ Bang methods mark the field as required. For example, `query! :page, Integer` means the request must include `page`, and the value must not be `nil`. Blank values are still allowed unless you set `blank: false`.
203
+
204
+ If you prefer not to use bang methods, you can also write `required: true`:
205
+
206
+ ```ruby
207
+ query :page, Integer, required: true
208
+ json data: {
209
+ title: { type: String, required: true }
210
+ }
211
+ ```
212
212
 
213
213
  Batch declaration forms:
214
214
 
@@ -237,7 +237,7 @@ in_query!(
237
237
  )
238
238
  ```
239
239
 
240
- #### request body
240
+ #### Request body
241
241
 
242
242
  General form:
243
243
 
@@ -249,11 +249,9 @@ Convenience helpers:
249
249
 
250
250
  ```ruby
251
251
  json data: { name!: String }
252
-
253
252
  json! data: { name!: String }
254
253
 
255
254
  form data: { file!: File, position: String }
256
-
257
255
  form! data: { file!: File }
258
256
  ```
259
257
 
@@ -263,16 +261,23 @@ Single multipart field helper:
263
261
  data :file, File
264
262
  ```
265
263
 
266
- For `body/body!`, `json/json!`, and `form/form!`, the bang form is currently kept for DSL compatibility. At runtime they all contribute to the same body contract, and root-body requiredness is not yet enforced as a separate rule.
264
+ Notes:
267
265
 
268
- #### OpenAPI
266
+ 1. When multiple `body/body!`, `json/json!`, or `form/form!` declarations are used:
267
+ - declarations with the same media type are merged
268
+ - if multiple media types are declared, the generated OpenAPI document will emit multiple media types
269
+ - field validation and coercion do not distinguish between media types, and always read values from Rails `params`
270
+
271
+ `body!`, `json!`, and `form!` make the root request body required at runtime. You can also write `required: true` on `body`, `json`, or `form` if you prefer not to use bang methods.
272
+
273
+ #### `openapi false`
274
+
275
+ You can also opt an action out of OpenAPI generation from either `doc` or `doc_dry`:
269
276
 
270
277
  ```ruby
271
278
  openapi false
272
279
  ```
273
280
 
274
- Use this when an action should stay out of the generated OpenAPI document. It also works inside `doc_dry`.
275
-
276
281
  #### Scope
277
282
 
278
283
  Use `scope` when you want a grouped view that spans multiple request locations:
@@ -283,6 +288,7 @@ doc {
283
288
  query :user_id, Integer
284
289
  form data: { name: String }
285
290
  }
291
+ form data: { not_in_scope: String }
286
292
  }
287
293
  ```
288
294
 
@@ -292,16 +298,53 @@ Then read it from `px.scope`:
292
298
  px.scope[:user] # => { user_id: 1, name: "Tom" }
293
299
  ```
294
300
 
295
- #### Response
301
+ You can also trim custom scope buckets with `compact:` or `compact_blank:`:
302
+
303
+ ```ruby
304
+ doc {
305
+ scope(:search, compact: true) {
306
+ query :page, Integer, transform: -> { nil }, px: :page_number
307
+ query :keyword, String
308
+ }
309
+
310
+ scope(:filters, compact_blank: true) {
311
+ query :q, String, transform: :strip
312
+ query :nickname, String, transform: -> { "" }
313
+ }
314
+ }
315
+
316
+ px.scope[:search] # => { keyword: "rails" }
317
+ px.scope[:filters] # => { q: "ruby" }
318
+ ```
319
+
320
+ These options only apply to the custom `px.scope[:name]` bucket defined by that `scope`, and use shallow hash compaction.
321
+
322
+ #### Response
296
323
 
297
324
  ```ruby
298
325
  response 200, desc: "success"
299
326
  response 422, "validation failed"
300
- resp 400, "bad request"
327
+ response 200, :json, data: { code!: Integer, result: Object }
328
+
301
329
  error 401, "unauthorized"
330
+ error 503, { code!: Integer, message!: String } # error data schema
331
+ error 503, { code: 1000, message: "invalid params" } # unnamed error example
332
+ error 503, invalid_params: { code: 1000, message: "invalid params" } # named error example
333
+ # declare multiple named examples in batch
334
+ errors 503, {
335
+ invalid_params: { code: 1000, message: "invalid params" },
336
+ network_error: { code: 1001, message: "network error" }
337
+ }
338
+ errors 503, network_error: { code: 1001 }, upstream_timeout: { code: 1002 } # braces are also optional
339
+
302
340
  ```
303
341
 
304
- Response declarations are stored as metadata now. They are not yet used to render responses automatically.
342
+ Response declarations are stored as metadata and are emitted in OpenAPI. They do not render responses automatically at runtime.
343
+
344
+ Notes:
345
+
346
+ 1. `response`, `error`, and `errors` default `media_type` to `:json` and this default is configurable.
347
+ 2. If examples are declared without an explicit schema, ActionSpec infers the response schema from the example payloads for OpenAPI generation.
305
348
 
306
349
  ## Schemas
307
350
 
@@ -324,7 +367,11 @@ Meaning of `!`:
324
367
 
325
368
  - `query!`, `path!`, `header!`, `cookie!` mark the parameter itself as required
326
369
  - keys such as `name!:` or `nickname!:` mark nested object fields as required
327
- - `body!`, `json!`, and `form!` are currently accepted for DSL consistency, but today they behave the same as the non-bang form at runtime
370
+ - `body!`, `json!`, and `form!` mark the root request body as required
371
+
372
+ You can also use `required: true` instead of bang syntax for parameters, nested fields, and the root request body.
373
+
374
+ `required` in ActionSpec means "present and not `nil`". It does not reject blank strings by itself. If you want to reject blank values, use `blank: false` or `allow_blank: false`.
328
375
 
329
376
  #### Field Types
330
377
 
@@ -339,24 +386,38 @@ Scalar types currently supported by validation/coercion:
339
386
  - `DateTime`
340
387
  - `Time`
341
388
  - `File`
342
- - `Object`
389
+ - `Object` / `Hash`
343
390
 
344
- Nested forms:
391
+ ```ruby
392
+ query :page, Integer
393
+ form data: { file: File }
394
+ ```
395
+
396
+ Object and Nested Object forms:
345
397
 
346
398
  ```ruby
347
399
  json data: {
348
- tags: [String],
349
- profile: {
350
- nickname!: String
351
- },
352
- settings: { type: Object },
353
- users: [{ id: Integer }]
400
+ settings: Object,
401
+ # == settings: { type: Object }
402
+ # == settings: { type: Hash }
403
+ # == settings: { type: { } }
404
+
405
+ profile: { nickname!: String },
406
+ tags: [],
407
+ # == tags: { type: [] }
408
+ foo: [{ id: Integer }],
409
+
410
+ users: {
411
+ type: { name!: String },
412
+ default: { name: "Tom" },
413
+ transform: -> { { name: it[:name].downcase } }
414
+ }
354
415
  }
355
416
  ```
356
417
 
357
- #### Field Options
418
+ When you write `xx: { type: ... }`, the type of `xx` comes from `type`. In other words, `type` is a reserved schema keyword here, not a normal nested field name.
358
419
 
359
- These options are currently used by the validator:
420
+ #### Field Options
360
421
 
361
422
  ```ruby
362
423
  query :page, Integer, default: 1
@@ -364,18 +425,48 @@ query :today, Date, default: -> { Time.current.to_date }
364
425
  query :status, String, enum: %w[draft published]
365
426
  query :score, Integer, range: { ge: 1, le: 5 }
366
427
  query :slug, String, pattern: /\A[a-z\-]+\z/
428
+ query :title, String, blank: false # or allow_blank: false
429
+
430
+ query :nickname, String, transform: :downcase
431
+ query :page, Integer, transform: -> { it + 1 }, px: :page_number
432
+ query :request_id, String, px_key: :trace_id
433
+ query :end_at, Integer, validate: -> { it >= px[:start_at] }
367
434
  ```
368
435
 
369
- These options are currently used by OpenAPI generation, but are not yet used by the runtime validator:
436
+ Notes:
437
+
438
+ - `transform` accepts a `Symbol` or a `Proc` and runs **after coercion**, before the value is written into `px`
439
+ - `px` and `px_key` customize the key name written into `px`; `px` is the short form of `px_key`
440
+ - `validate` accepts a `Proc` and runs **after all parameters have been resolved, coerced, transformed, and written into `px`**
441
+ - `validate` runs in the current controller context, so it can read `px` and call methods such as `current_user`
442
+ - when `validate` returns `false` or `nil`, the field adds an `invalid` error
443
+
444
+ About nested object fields
445
+
446
+ Inner field options run first, and the outer object field runs last.
447
+ In other words, nested `transform` / `px` first participate in building the object, and after the whole object has been built, the outer field receives that final object and continues processing it.
448
+ Only after the whole object tree has finished resolving, coercing, and transforming does ActionSpec enter the post-`validate` phase: inner field `validate` callbacks run first, and outer object field `validate` callbacks run afterwards.
449
+
450
+ ```ruby
451
+ json data: {
452
+ user: {
453
+ type: {
454
+ name: { type: String, transform: :strip, px: :nickname }
455
+ },
456
+ transform: -> { { name: it[:nickname].downcase } }
457
+ }
458
+ }
459
+
460
+ px[:user] # => { "name" => "tom" }
461
+ ```
370
462
 
371
- - `desc`
372
- - `example`
373
- - `examples`
463
+ These options are used by OpenAPI generation:
374
464
 
375
- These options are not yet used by either the runtime validator or OpenAPI generation:
465
+ ```ruby
466
+ query :page, Integer, desc: "page number", example: 1, examples: [1, 2, 3]
467
+ ```
376
468
 
377
- - `allow_nil`
378
- - `allow_blank`
469
+ If an OpenAPI-facing option such as `default` cannot be converted into YAML, for example `default: -> { ... }`, it will be omitted from the generated OpenAPI document.
379
470
 
380
471
  #### Schemas From ActiveRecord
381
472
 
@@ -405,10 +496,16 @@ You can also limit the exported fields:
405
496
  User.schemas(only: %i[name phone role])
406
497
  ```
407
498
 
499
+ `bang:` defaults to `true`, so required fields are emitted as bang keys such as `"name!"`. If you prefer plain keys, you can pass `bang: false`, and required fields will be emitted as `required: true` instead:
500
+
501
+ ```ruby
502
+ User.schemas(bang: false)
503
+ ```
504
+
408
505
  ActionSpec extracts schema-relevant information from ActiveRecord / ActiveModel when available, including:
409
506
 
410
507
  - field type
411
- - requiredness, rendered as bang keys such as `"name!"`
508
+ - requiredness, rendered either as bang keys such as `"name!"` or as `required: true` when `bang: false`
412
509
  - enum values from `enum`
413
510
  - `default`
414
511
  - `desc` from column comments
@@ -425,6 +522,13 @@ User.schemas
425
522
  # "phone!" => { type: String, length: { maximum: 13 }, pattern: /\A1\d{10}\z/ },
426
523
  # "role" => { type: String, enum: %w[admin member visitor] }
427
524
  # }
525
+
526
+ User.schemas(bang: false)
527
+ # {
528
+ # "name" => { type: String, required: true, desc: "user name", length: { maximum: 20 } },
529
+ # "phone" => { type: String, required: true, length: { maximum: 13 }, pattern: /\A1\d{10}\z/ },
530
+ # "role" => { type: String, enum: %w[admin member visitor] }
531
+ # }
428
532
  ```
429
533
 
430
534
  #### Type And Boundary Matrix
@@ -484,6 +588,7 @@ This hook also skips actions without a matching `doc`, so it is safe to declare
484
588
  ### Reading Processed Values With `px`
485
589
 
486
590
  `px` stores the processed values produced by ActionSpec. With `validate_params!` they stay raw; with `validate_and_coerce_params!` they are coerced values.
591
+
487
592
  Because `px` is still a hash, you can also use helpers such as `px.slice(...)` to simplify parameter access code.
488
593
 
489
594
  ```ruby
@@ -502,12 +607,13 @@ px.scope[:query]
502
607
  px.scope[:body]
503
608
  px.scope[:headers]
504
609
  px.scope[:cookies]
610
+ px.scope[:the_scope_you_defined]
505
611
  ```
506
612
 
507
613
  Notes:
508
614
 
509
615
  - every declared field from path/query/body is also flattened into the top-level `px[:field]`
510
- - custom `scope(:name)` buckets are also exposed through `px.scope[:name]`
616
+ - `px` itself is an `ActiveSupport::HashWithIndifferentAccess`, and nested object values such as `px[:profile]` are also returned as indifferent hashes
511
617
  - headers and cookies stay inside their own grouped buckets; for example, `px[:Authorization]` is not a top-level shortcut
512
618
  - header keys are stored in lowercase dashed form, but reading remains compatible with original forms such as `Authorization` and `HTTP_AUTHORIZATION`, for example:
513
619
 
@@ -519,6 +625,28 @@ px.scope[:headers]["HTTP_AUTHORIZATION"]
519
625
 
520
626
  - original `params` are not mutated
521
627
 
628
+ ### `px` is Whitelist Extraction
629
+
630
+ ```ruby
631
+ json data: {
632
+ profile: {
633
+ nickname: String
634
+ }
635
+ }
636
+
637
+ # request params
638
+ {
639
+ profile: {
640
+ nickname: "Neo",
641
+ role: "admin"
642
+ }
643
+ }
644
+
645
+ px[:profile] # => { "nickname" => "Neo" }
646
+ ```
647
+
648
+ Undeclared fields are filtered out, including extra keys inside nested objects.
649
+
522
650
  ### Errors
523
651
 
524
652
  Validation errors are stored in `ActiveModel::Errors`.
@@ -555,6 +683,7 @@ ActionSpec.configure { |config|
555
683
  config.open_api_title = "My API"
556
684
  config.open_api_version = "2026.03"
557
685
  config.open_api_server_url = "https://api.example.com"
686
+ config.default_response_media_type = :json
558
687
 
559
688
  config.error_messages[:invalid_type] = ->(_attribute, options) {
560
689
  "should be coercible to #{options.fetch(:expected)}"
@@ -564,29 +693,13 @@ ActionSpec.configure { |config|
564
693
 
565
694
  Available config keys:
566
695
 
567
- - `invalid_parameters_exception_class`
568
- Default: `ActionSpec::InvalidParameters`.
569
- Controls which exception class is raised when validation fails.
570
-
571
- - `error_messages`
572
- Default: `{}`.
573
- Lets you override error messages by error type, or by attribute plus error type.
574
-
575
- - `open_api_output`
576
- Default: `"docs/openapi.yml"`.
577
- Controls where `bin/rails action_spec:gen` writes the generated OpenAPI document.
578
-
579
- - `open_api_title`
580
- Default: `nil`.
581
- Sets the default OpenAPI `info.title` used by `bin/rails action_spec:gen`.
582
-
583
- - `open_api_version`
584
- Default: `nil`.
585
- Sets the default OpenAPI `info.version` used by `bin/rails action_spec:gen`.
586
-
587
- - `open_api_server_url`
588
- Default: `nil`.
589
- Sets the default server URL emitted in the generated OpenAPI document.
696
+ - `invalid_parameters_exception_class`: Default `ActionSpec::InvalidParameters`; controls which exception class is raised when validation fails.
697
+ - `error_messages`: Default `{}`; lets you override error messages by error type, or by attribute plus error type.
698
+ - `open_api_output`: Default `"docs/openapi.yml"`; controls where `bin/rails action_spec:gen` writes the generated OpenAPI document.
699
+ - `open_api_title`: Default `nil`; sets the default OpenAPI `info.title` used by `bin/rails action_spec:gen`.
700
+ - `open_api_version`: Default `nil`; sets the default OpenAPI `info.version` used by `bin/rails action_spec:gen`.
701
+ - `open_api_server_url`: Default `nil`; sets the default server URL emitted in the generated OpenAPI document.
702
+ - `default_response_media_type`: Default `:json`; sets the default response media type used by `response`, `error`, and `errors` when no media type is passed explicitly.
590
703
 
591
704
  ### I18n
592
705
 
@@ -625,7 +738,8 @@ When using AI tools to generate Rails controller code, and the change involves p
625
738
  - when a batch has 3 fields or fewer and does not contain nested hashes, prefer a single-line style, for example:
626
739
  - `json data: { type: String, required: true }`
627
740
  - `in_query(name: String, value: String)` (prefer `in_xxx(...)` batch declarations over multiple `xx` DSL lines when possible)
628
- - use `doc_dry`, `scope`, and `px.slice` to reduce repetition in controllers
741
+ - use `!` but not `required: true`
742
+ - use `doc_dry`, `scope`, and `transform`、`px(px_key)`、`px.slice` to keep controller concise
629
743
  - when request parameters match model declarations, prefer `.schemas` to keep `doc` concise
630
744
 
631
745
  ## What Is Not Implemented Yet
@@ -636,7 +750,6 @@ When using AI tools to generate Rails controller code, and the change involves p
636
750
  - parameter-level `style`, `explode`, `allowReserved`, `examples`, and richer header/cookie serialization controls
637
751
  - request body `encoding`
638
752
  - multiple request/response media types beyond the current direct DSL mapping
639
- - response body schema generation; current `response` / `resp` / `error` declarations only generate response descriptions
640
753
  - response headers
641
754
  - response links
642
755
  - callbacks
@@ -647,7 +760,7 @@ When using AI tools to generate Rails controller code, and the change involves p
647
760
  - top-level `tags`
648
761
  - top-level `externalDocs`
649
762
  - `jsonSchemaDialect`
650
- - richer schema keywords beyond the current subset, including nullable/blank semantics, object-level constraints, and composition keywords such as `oneOf`, `anyOf`, `allOf`, and `not`
763
+ - richer schema keywords beyond the current subset, including object-level constraints, and composition keywords such as `oneOf`, `anyOf`, `allOf`, and `not`
651
764
 
652
765
  ## Contributing
653
766
 
@@ -3,7 +3,7 @@
3
3
  module ActionSpec
4
4
  class Configuration
5
5
  attr_accessor :invalid_parameters_exception_class, :open_api_output, :open_api_title, :open_api_version,
6
- :open_api_server_url
6
+ :open_api_server_url, :default_response_media_type
7
7
  attr_reader :error_messages
8
8
 
9
9
  def initialize
@@ -12,6 +12,7 @@ module ActionSpec
12
12
  @open_api_title = nil
13
13
  @open_api_version = nil
14
14
  @open_api_server_url = nil
15
+ @default_response_media_type = :json
15
16
  @error_messages = ActiveSupport::HashWithIndifferentAccess.new
16
17
  end
17
18
 
@@ -33,6 +34,7 @@ module ActionSpec
33
34
  copy.open_api_title = open_api_title
34
35
  copy.open_api_version = open_api_version
35
36
  copy.open_api_server_url = open_api_server_url
37
+ copy.default_response_media_type = default_response_media_type
36
38
  copy.error_messages = error_messages.deep_dup
37
39
  end
38
40
  end