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 +4 -4
- data/README.md +233 -120
- data/lib/action_spec/configuration.rb +3 -1
- data/lib/action_spec/doc/dsl.rb +61 -32
- data/lib/action_spec/doc/endpoint.rb +109 -7
- data/lib/action_spec/open_api/operation.rb +1 -1
- data/lib/action_spec/open_api/schema.rb +106 -1
- data/lib/action_spec/schema/active_record.rb +6 -5
- data/lib/action_spec/schema/array_of.rb +5 -1
- data/lib/action_spec/schema/base.rb +13 -4
- data/lib/action_spec/schema/field.rb +66 -3
- data/lib/action_spec/schema/object_of.rb +6 -2
- data/lib/action_spec/schema/resolver.rb +27 -4
- data/lib/action_spec/schema/scalar.rb +2 -2
- data/lib/action_spec/schema/type_caster.rb +1 -1
- data/lib/action_spec/schema.rb +83 -13
- data/lib/action_spec/validation_result.rb +10 -0
- data/lib/action_spec/validator/runner.rb +121 -1
- data/lib/action_spec/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9218555ca7d33709a7a5b22528f6900358e36247c123e521f2342d7a36ac02b5
|
|
4
|
+
data.tar.gz: e771af58669e707ccbcc922b6d9afa726a2b25817ab4836e01ce1154bd22344b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d03ee8f19de7c1c874302344eadf224f9aa3fba2dab228f2f4b44448a2865ba53dd97385587b6b9691308e4a987048b1eab6e5f55a1b03ef93424a643857a0c
|
|
7
|
+
data.tar.gz: 357fe37dc9a591740079c76dd3e4238a9485cb0d1e57ab020bed09bf737d1937ef22b777f8b062f78473fe497965c2df754ee3858a97f77bdd59674002cdda69
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# ActionSpec
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 :
|
|
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,
|
|
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.
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
####
|
|
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
|
-
|
|
264
|
+
Notes:
|
|
267
265
|
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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!`
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
- `example`
|
|
373
|
-
- `examples`
|
|
463
|
+
These options are used by OpenAPI generation:
|
|
374
464
|
|
|
375
|
-
|
|
465
|
+
```ruby
|
|
466
|
+
query :page, Integer, desc: "page number", example: 1, examples: [1, 2, 3]
|
|
467
|
+
```
|
|
376
468
|
|
|
377
|
-
- `
|
|
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
|
-
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
- `
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
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
|
|
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
|