action_spec 1.4.0 → 1.6.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 +222 -35
- data/lib/action_spec/configuration.rb +3 -1
- data/lib/action_spec/doc/dsl.rb +2 -1
- data/lib/action_spec/doc/endpoint.rb +22 -1
- data/lib/action_spec/schema/active_record.rb +159 -23
- data/lib/action_spec/schema/array_of.rb +12 -4
- data/lib/action_spec/schema/base.rb +24 -14
- data/lib/action_spec/schema/field.rb +49 -3
- data/lib/action_spec/schema/object_of.rb +13 -5
- data/lib/action_spec/schema/resolver.rb +17 -10
- data/lib/action_spec/schema/scalar.rb +11 -3
- data/lib/action_spec/schema.rb +47 -8
- data/lib/action_spec/validation_result.rb +13 -1
- data/lib/action_spec/validator/runner.rb +104 -0
- 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: d4483d08f3dbf8917159d539ee9b064366551409ae646714def0b89595484428
|
|
4
|
+
data.tar.gz: 065af0d1f835124179afef0a756515b6184598cd35ef217a96615f18b2098cbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71ae3fd218312cb0e788f7a5affad8e31da416546e319852509d260490fe261009be24fc51cedfc5948c8c7e9cee04285b50019e613316f3a96605d7cee98b1f
|
|
7
|
+
data.tar.gz: 7aaee031ed042a3631c952789ccf4ca37db7f7a7f53fcb6a45f1a0e8c4badad005cd4e8774fad58d467c8b2f49d543f4d499407b98cad4dde8fe4e6dbe7cc197
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Concise and Powerful API Documentation Solution for Rails. [中文](README_zh.md
|
|
|
16
16
|
2. [`doc_dry`](#doc_dry)
|
|
17
17
|
3. [DSL Inside `doc`](#dsl-inside-doc)
|
|
18
18
|
1. [Parameter](#parameter)
|
|
19
|
-
2. [
|
|
19
|
+
2. [Request body](#request-body)
|
|
20
20
|
3. [`openapi false`](#openapi-false)
|
|
21
21
|
4. [Scope](#scope)
|
|
22
22
|
5. [Response](#response)
|
|
@@ -29,7 +29,8 @@ Concise and Powerful API Documentation Solution for Rails. [中文](README_zh.md
|
|
|
29
29
|
4. [Parameter Validation And Type Coercion](#parameter-validation-and-type-coercion)
|
|
30
30
|
1. [Validation Flow](#validation-flow)
|
|
31
31
|
2. [Reading Processed Values With `px`](#reading-processed-values-with-px)
|
|
32
|
-
3. [
|
|
32
|
+
3. [`px` is Whitelist Extraction](#px-is-whitelist-extraction)
|
|
33
|
+
4. [Errors](#errors)
|
|
33
34
|
5. [Configuration And I18n](#configuration-and-i18n)
|
|
34
35
|
1. [Configuration](#configuration)
|
|
35
36
|
2. [I18n](#i18n)
|
|
@@ -44,25 +45,26 @@ class UsersController < ApplicationController
|
|
|
44
45
|
doc {
|
|
45
46
|
header :Authorization, String
|
|
46
47
|
path :account_id, Integer
|
|
47
|
-
query :locale, String, default: "zh-CN"
|
|
48
|
-
query :
|
|
48
|
+
query :locale, String, default: "zh-CN", transform: -> { it.downcase }
|
|
49
|
+
query :key_number, Integer, default: -> { it + 1 }, px: :key
|
|
49
50
|
|
|
50
51
|
form data: {
|
|
51
|
-
name!: String,
|
|
52
|
-
age: Integer,
|
|
52
|
+
name!: { type: String, transform: :strip },
|
|
53
53
|
birthday: Date,
|
|
54
54
|
admin: { type: :boolean, default: false },
|
|
55
|
-
tags: [String],
|
|
56
|
-
profile: {
|
|
57
|
-
nickname!: String
|
|
58
|
-
}
|
|
55
|
+
tags: [{ id: Integer, content!: { type: String, blank: false } }],
|
|
56
|
+
profile: { nickname!: String }
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
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
|
+
}
|
|
62
64
|
}
|
|
63
65
|
def create
|
|
64
|
-
User.
|
|
65
|
-
|
|
66
|
+
User.find(px[:account_id]).update!(
|
|
67
|
+
key: px[:key], name: px[:name],
|
|
66
68
|
**px.slice(:birthday, :admin, :profile)
|
|
67
69
|
)
|
|
68
70
|
end
|
|
@@ -199,6 +201,16 @@ cookie! :remember_token, String
|
|
|
199
201
|
|
|
200
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`.
|
|
201
203
|
|
|
204
|
+
You can also change that default globally:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
ActionSpec.configure do |config|
|
|
208
|
+
config.required_allow_blank = false
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With `required_allow_blank = false`, required fields reject blank strings unless that field explicitly sets `blank:` or `allow_blank:`.
|
|
213
|
+
|
|
202
214
|
If you prefer not to use bang methods, you can also write `required: true`:
|
|
203
215
|
|
|
204
216
|
```ruby
|
|
@@ -235,7 +247,7 @@ in_query!(
|
|
|
235
247
|
)
|
|
236
248
|
```
|
|
237
249
|
|
|
238
|
-
####
|
|
250
|
+
#### Request body
|
|
239
251
|
|
|
240
252
|
General form:
|
|
241
253
|
|
|
@@ -296,7 +308,28 @@ Then read it from `px.scope`:
|
|
|
296
308
|
px.scope[:user] # => { user_id: 1, name: "Tom" }
|
|
297
309
|
```
|
|
298
310
|
|
|
299
|
-
|
|
311
|
+
You can also trim custom scope buckets with `compact:` or `compact_blank:`:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
doc {
|
|
315
|
+
scope(:search, compact: true) {
|
|
316
|
+
query :page, Integer, transform: -> { nil }, px: :page_number
|
|
317
|
+
query :keyword, String
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
scope(:filters, compact_blank: true) {
|
|
321
|
+
query :q, String, transform: :strip
|
|
322
|
+
query :nickname, String, transform: -> { "" }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
px.scope[:search] # => { keyword: "rails" }
|
|
327
|
+
px.scope[:filters] # => { q: "ruby" }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
These options only apply to the custom `px.scope[:name]` bucket defined by that `scope`, and use shallow hash compaction.
|
|
331
|
+
|
|
332
|
+
#### Response
|
|
300
333
|
|
|
301
334
|
```ruby
|
|
302
335
|
response 200, desc: "success"
|
|
@@ -348,7 +381,9 @@ Meaning of `!`:
|
|
|
348
381
|
|
|
349
382
|
You can also use `required: true` instead of bang syntax for parameters, nested fields, and the root request body.
|
|
350
383
|
|
|
351
|
-
`required` in ActionSpec means "present and not `nil`".
|
|
384
|
+
`required` in ActionSpec means "present and not `nil`". By default it does not reject blank strings. You can keep that default, change it globally with `config.required_allow_blank`, or override it per field with `blank:` / `allow_blank:`.
|
|
385
|
+
|
|
386
|
+
When blank values are allowed, type coercion does not fail just because the input is an empty string. If the field can still carry a meaningful blank value, such as `String`, the original blank string stays in `px`. Otherwise, ActionSpec stores `nil` for that field. For example, `""` stays `""` for `String`, but becomes `nil` for `Date`.
|
|
352
387
|
|
|
353
388
|
#### Field Types
|
|
354
389
|
|
|
@@ -363,21 +398,37 @@ Scalar types currently supported by validation/coercion:
|
|
|
363
398
|
- `DateTime`
|
|
364
399
|
- `Time`
|
|
365
400
|
- `File`
|
|
366
|
-
- `Object`
|
|
401
|
+
- `Object` / `Hash`
|
|
367
402
|
|
|
368
|
-
|
|
403
|
+
```ruby
|
|
404
|
+
query :page, Integer
|
|
405
|
+
form data: { file: File }
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Object and Nested Object forms:
|
|
369
409
|
|
|
370
410
|
```ruby
|
|
371
411
|
json data: {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
412
|
+
settings: Object,
|
|
413
|
+
# == settings: { type: Object }
|
|
414
|
+
# == settings: { type: Hash }
|
|
415
|
+
# == settings: { type: { } }
|
|
416
|
+
|
|
417
|
+
profile: { nickname!: String },
|
|
418
|
+
tags: [],
|
|
419
|
+
# == tags: { type: [] }
|
|
420
|
+
foo: [{ id: Integer }],
|
|
421
|
+
|
|
422
|
+
users: {
|
|
423
|
+
type: { name!: String },
|
|
424
|
+
default: { name: "Tom" },
|
|
425
|
+
transform: -> { { name: it[:name].downcase } }
|
|
426
|
+
}
|
|
378
427
|
}
|
|
379
428
|
```
|
|
380
429
|
|
|
430
|
+
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.
|
|
431
|
+
|
|
381
432
|
#### Field Options
|
|
382
433
|
|
|
383
434
|
```ruby
|
|
@@ -386,17 +437,77 @@ query :today, Date, default: -> { Time.current.to_date }
|
|
|
386
437
|
query :status, String, enum: %w[draft published]
|
|
387
438
|
query :score, Integer, range: { ge: 1, le: 5 }
|
|
388
439
|
query :slug, String, pattern: /\A[a-z\-]+\z/
|
|
389
|
-
query :title, String, blank: false
|
|
440
|
+
query :title, String, blank: false
|
|
390
441
|
|
|
391
442
|
query :nickname, String, transform: :downcase
|
|
392
443
|
query :page, Integer, transform: -> { it + 1 }, px: :page_number
|
|
393
|
-
query :
|
|
444
|
+
query :end_at, Integer, validate: -> { current_user && it >= px[:start_at] }
|
|
445
|
+
query :birthday, Date, error: "birthday error"
|
|
394
446
|
```
|
|
395
447
|
|
|
396
|
-
|
|
448
|
+
- `required`
|
|
449
|
+
- Marks the field as required.
|
|
450
|
+
- Can replace bang syntax when you do not use `name!:`.
|
|
451
|
+
- `default`
|
|
452
|
+
- Default value used when the field is missing.
|
|
453
|
+
- Can be a literal or `-> { }`.
|
|
454
|
+
- `enum`
|
|
455
|
+
- Restricts the field to values from a fixed set.
|
|
456
|
+
- `range`
|
|
457
|
+
- Numeric range constraints.
|
|
458
|
+
- Available: `ge` / `gt` / `le` / `lt`
|
|
459
|
+
- `pattern`
|
|
460
|
+
- Regex constraint.
|
|
461
|
+
- `length`
|
|
462
|
+
- Length constraints.
|
|
463
|
+
- Available: `minimum` / `maximum` / `is`
|
|
464
|
+
- `blank` / `allow_blank`
|
|
465
|
+
- Controls whether blank values are allowed.
|
|
466
|
+
- For fields such as `Date`, if blank is allowed, no type coercion is applied and the value becomes `nil`.
|
|
467
|
+
|
|
468
|
+
- `desc`
|
|
469
|
+
- Used only for OpenAPI description generation.
|
|
470
|
+
- `example`
|
|
471
|
+
- Used only for generating a single OpenAPI example.
|
|
472
|
+
- `examples`
|
|
473
|
+
- Used only for generating multiple OpenAPI examples.
|
|
474
|
+
|
|
475
|
+
- `transform`
|
|
476
|
+
- Applies one more custom transformation to the **already-coerced value**.
|
|
477
|
+
- Accepts a `Symbol` or a `Proc`.
|
|
478
|
+
- `px` / `px_key`
|
|
479
|
+
- Customize the key name used when the parameter is written into `px`.
|
|
480
|
+
- `validate`
|
|
481
|
+
- Accepts a `Proc`.
|
|
482
|
+
- Runs **after all parameters have finished resolving, coercion, transform, and writing into `px`**.
|
|
483
|
+
- Runs in the current controller context, so it can read `px` and directly call controller methods such as `current_user`.
|
|
484
|
+
- When `validate` returns `false` or `nil`, the field adds an `invalid` error.
|
|
485
|
+
- `error` / `error_message`
|
|
486
|
+
- Override the error message used when that field fails validation or coercion.
|
|
487
|
+
- Supported forms:
|
|
488
|
+
- `String`
|
|
489
|
+
- `-> { }`
|
|
490
|
+
- `->(error, value) { }`
|
|
491
|
+
- Field-level `error` / `error_message` have higher priority than global `config.error_messages`.
|
|
492
|
+
|
|
493
|
+
About nested object fields
|
|
494
|
+
|
|
495
|
+
Inner field options run first, and the outer object field runs last.
|
|
496
|
+
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.
|
|
497
|
+
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.
|
|
397
498
|
|
|
398
|
-
|
|
399
|
-
|
|
499
|
+
```ruby
|
|
500
|
+
json data: {
|
|
501
|
+
user: {
|
|
502
|
+
type: {
|
|
503
|
+
name: { type: String, transform: :strip, px: :nickname }
|
|
504
|
+
},
|
|
505
|
+
transform: -> { { name: it[:nickname].downcase } }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
px[:user] # => { "name" => "tom" }
|
|
510
|
+
```
|
|
400
511
|
|
|
401
512
|
These options are used by OpenAPI generation:
|
|
402
513
|
|
|
@@ -420,7 +531,7 @@ class UsersController < ApplicationController
|
|
|
420
531
|
end
|
|
421
532
|
```
|
|
422
533
|
|
|
423
|
-
`User.schemas` returns a hash that can be passed directly into `form data:`, `json data:`, or `body`.
|
|
534
|
+
`User.schemas` returns a symbol-keyed hash that can be passed directly into `form data:`, `json data:`, or `body`.
|
|
424
535
|
|
|
425
536
|
By default, it includes all model fields:
|
|
426
537
|
|
|
@@ -434,10 +545,46 @@ You can also limit the exported fields:
|
|
|
434
545
|
User.schemas(only: %i[name phone role])
|
|
435
546
|
```
|
|
436
547
|
|
|
548
|
+
Or exclude specific fields:
|
|
549
|
+
|
|
550
|
+
```ruby
|
|
551
|
+
User.schemas(except: %i[phone role])
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
When `only:` and `except:` are used together, ActionSpec applies `except:` after `only:`.
|
|
555
|
+
|
|
556
|
+
You can also extract validators for a specific validation context:
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
User.schemas(on: :create)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
You can also override requiredness in the exported schema:
|
|
563
|
+
When `required:` is an array, only the listed fields are treated as required, and every other exported field is treated as non-required.
|
|
564
|
+
|
|
565
|
+
```ruby
|
|
566
|
+
User.schemas(required: true)
|
|
567
|
+
User.schemas(required: false)
|
|
568
|
+
User.schemas(required: %i[name role])
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
You can also merge custom schema fragments into the exported fields:
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
User.schemas(merge: { name: { required: false, desc: "nickname" } })
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
`bang:` defaults to `true`, so required fields are emitted as bang keys such as `name!:`. `only:` and `except:` both accept plain names or bang-style names such as `phone!`. If you prefer plain keys, you can pass `bang: false`, and required fields will be emitted as `required: true` instead:
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
User.schemas(bang: false)
|
|
581
|
+
```
|
|
582
|
+
|
|
437
583
|
ActionSpec extracts schema-relevant information from ActiveRecord / ActiveModel when available, including:
|
|
438
584
|
|
|
439
585
|
- field type
|
|
440
|
-
- requiredness, rendered as bang keys such as `
|
|
586
|
+
- requiredness, rendered either as bang keys such as `name!:` or as `required: true` when `bang: false`
|
|
587
|
+
- `allow_blank: false` from presence validators unless that validator explicitly allows blank
|
|
441
588
|
- enum values from `enum`
|
|
442
589
|
- `default`
|
|
443
590
|
- `desc` from column comments
|
|
@@ -445,15 +592,19 @@ ActionSpec extracts schema-relevant information from ActiveRecord / ActiveModel
|
|
|
445
592
|
- `range` from numericality validators
|
|
446
593
|
- `length` from length validators and string column limits
|
|
447
594
|
|
|
595
|
+
Conditional validators with `if:` or `unless:` are skipped during schema extraction, because they cannot be represented as unconditional static schema rules. Validators with `on:` / `except_on:` are skipped by default, but can be extracted by passing `schemas(on: ...)`. The `required:` option overrides the requiredness of exported fields only; it does not remove other extracted constraints such as `allow_blank: false`. The `merge:` option deep-merges custom fragments into each extracted field definition.
|
|
596
|
+
|
|
448
597
|
Example output:
|
|
449
598
|
|
|
450
599
|
```ruby
|
|
451
600
|
User.schemas
|
|
452
601
|
# {
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
#
|
|
602
|
+
# name!: { type: String, desc: "user name", length: { maximum: 20 } },
|
|
603
|
+
# phone!: { type: String, allow_blank: false, length: { maximum: 13 }, pattern: /\A1\d{10}\z/ },
|
|
604
|
+
# role: { type: String, enum: %w[admin member visitor] }
|
|
456
605
|
# }
|
|
606
|
+
User.schemas(bang: false)
|
|
607
|
+
# { name: { type: String, required: true, desc: "user name", length: { maximum: 20 } }, ... }
|
|
457
608
|
```
|
|
458
609
|
|
|
459
610
|
#### Type And Boundary Matrix
|
|
@@ -532,12 +683,13 @@ px.scope[:query]
|
|
|
532
683
|
px.scope[:body]
|
|
533
684
|
px.scope[:headers]
|
|
534
685
|
px.scope[:cookies]
|
|
686
|
+
px.scope[:the_scope_you_defined]
|
|
535
687
|
```
|
|
536
688
|
|
|
537
689
|
Notes:
|
|
538
690
|
|
|
539
691
|
- every declared field from path/query/body is also flattened into the top-level `px[:field]`
|
|
540
|
-
-
|
|
692
|
+
- `px` itself is an `ActiveSupport::HashWithIndifferentAccess`, and nested object values such as `px[:profile]` are also returned as indifferent hashes
|
|
541
693
|
- headers and cookies stay inside their own grouped buckets; for example, `px[:Authorization]` is not a top-level shortcut
|
|
542
694
|
- header keys are stored in lowercase dashed form, but reading remains compatible with original forms such as `Authorization` and `HTTP_AUTHORIZATION`, for example:
|
|
543
695
|
|
|
@@ -549,6 +701,28 @@ px.scope[:headers]["HTTP_AUTHORIZATION"]
|
|
|
549
701
|
|
|
550
702
|
- original `params` are not mutated
|
|
551
703
|
|
|
704
|
+
### `px` is Whitelist Extraction
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
json data: {
|
|
708
|
+
profile: {
|
|
709
|
+
nickname: String
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
# request params
|
|
714
|
+
{
|
|
715
|
+
profile: {
|
|
716
|
+
nickname: "Neo",
|
|
717
|
+
role: "admin"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
px[:profile] # => { "nickname" => "Neo" }
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Undeclared fields are filtered out, including extra keys inside nested objects.
|
|
725
|
+
|
|
552
726
|
### Errors
|
|
553
727
|
|
|
554
728
|
Validation errors are stored in `ActiveModel::Errors`.
|
|
@@ -631,6 +805,18 @@ ActionSpec.configure { |config|
|
|
|
631
805
|
}
|
|
632
806
|
```
|
|
633
807
|
|
|
808
|
+
If you want to override one specific field directly in the DSL, use `error` or `error_message` on that field:
|
|
809
|
+
|
|
810
|
+
```ruby
|
|
811
|
+
doc {
|
|
812
|
+
query! :page, Integer, error: "choose a page first"
|
|
813
|
+
query :role, String, validate: -> { false }, error_message: -> { "is not allowed for #{current_user}" }
|
|
814
|
+
json data: {
|
|
815
|
+
birthday!: { type: Date, error_message: ->(error, value) { "#{error}: #{value.inspect}" } }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
```
|
|
819
|
+
|
|
634
820
|
## AI Generation Style Guide
|
|
635
821
|
|
|
636
822
|
When using AI tools to generate Rails controller code, and the change involves parameter validation, type coercion, default values, or similar parameter contracts, these conventions work well with ActionSpec:
|
|
@@ -640,6 +826,7 @@ When using AI tools to generate Rails controller code, and the change involves p
|
|
|
640
826
|
- when a batch has 3 fields or fewer and does not contain nested hashes, prefer a single-line style, for example:
|
|
641
827
|
- `json data: { type: String, required: true }`
|
|
642
828
|
- `in_query(name: String, value: String)` (prefer `in_xxx(...)` batch declarations over multiple `xx` DSL lines when possible)
|
|
829
|
+
- use `!` but not `required: true`
|
|
643
830
|
- use `doc_dry`, `scope`, and `transform`、`px(px_key)`、`px.slice` to keep controller concise
|
|
644
831
|
- when request parameters match model declarations, prefer `.schemas` to keep `doc` concise
|
|
645
832
|
|
|
@@ -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, :default_response_media_type
|
|
6
|
+
:open_api_server_url, :default_response_media_type, :required_allow_blank
|
|
7
7
|
attr_reader :error_messages
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
@@ -13,6 +13,7 @@ module ActionSpec
|
|
|
13
13
|
@open_api_version = nil
|
|
14
14
|
@open_api_server_url = nil
|
|
15
15
|
@default_response_media_type = :json
|
|
16
|
+
@required_allow_blank = true
|
|
16
17
|
@error_messages = ActiveSupport::HashWithIndifferentAccess.new
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -35,6 +36,7 @@ module ActionSpec
|
|
|
35
36
|
copy.open_api_version = open_api_version
|
|
36
37
|
copy.open_api_server_url = open_api_server_url
|
|
37
38
|
copy.default_response_media_type = default_response_media_type
|
|
39
|
+
copy.required_allow_blank = required_allow_blank
|
|
38
40
|
copy.error_messages = error_messages.deep_dup
|
|
39
41
|
end
|
|
40
42
|
end
|
data/lib/action_spec/doc/dsl.rb
CHANGED
|
@@ -56,7 +56,8 @@ module ActionSpec
|
|
|
56
56
|
add_body(:form, { name => options.merge(type:) })
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def scope(name, &block)
|
|
59
|
+
def scope(name, compact: nil, compact_blank: nil, &block)
|
|
60
|
+
endpoint.request.register_scope(name, compact:, compact_blank:)
|
|
60
61
|
scopes.push(name.to_sym)
|
|
61
62
|
instance_exec(&block)
|
|
62
63
|
ensure
|
|
@@ -38,7 +38,7 @@ module ActionSpec
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
class Request
|
|
41
|
-
attr_reader :header, :path, :query, :cookie, :body, :body_media_types
|
|
41
|
+
attr_reader :header, :path, :query, :cookie, :body, :body_media_types, :scope_options
|
|
42
42
|
|
|
43
43
|
def initialize
|
|
44
44
|
@header = Location.new(:header)
|
|
@@ -47,6 +47,7 @@ module ActionSpec
|
|
|
47
47
|
@cookie = Location.new(:cookie)
|
|
48
48
|
@body = Location.new(:body)
|
|
49
49
|
@body_media_types = {}
|
|
50
|
+
@scope_options = ActiveSupport::HashWithIndifferentAccess.new
|
|
50
51
|
@body_required = false
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -63,6 +64,16 @@ module ActionSpec
|
|
|
63
64
|
(@body_media_types[media_type.to_sym] ||= Location.new(media_type.to_sym)).add(field.copy)
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
def register_scope(name, compact: nil, compact_blank: nil)
|
|
68
|
+
key = name.to_sym
|
|
69
|
+
@scope_options[key] = scope_options.fetch(key, {}).merge(
|
|
70
|
+
{
|
|
71
|
+
compact:,
|
|
72
|
+
compact_blank:
|
|
73
|
+
}.compact
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
66
77
|
def require_body!
|
|
67
78
|
@body_required = true
|
|
68
79
|
end
|
|
@@ -78,6 +89,7 @@ module ActionSpec
|
|
|
78
89
|
@cookie = other.cookie
|
|
79
90
|
@body = other.body
|
|
80
91
|
@body_media_types = other.body_media_types
|
|
92
|
+
@scope_options = other.scope_options
|
|
81
93
|
@body_required = other.body_required?
|
|
82
94
|
end
|
|
83
95
|
|
|
@@ -92,9 +104,14 @@ module ActionSpec
|
|
|
92
104
|
:@body_media_types,
|
|
93
105
|
body_media_types.transform_values(&:copy)
|
|
94
106
|
)
|
|
107
|
+
request.instance_variable_set(:@scope_options, scope_options.deep_dup)
|
|
95
108
|
request.instance_variable_set(:@body_required, body_required?)
|
|
96
109
|
end
|
|
97
110
|
end
|
|
111
|
+
|
|
112
|
+
def custom_validation?
|
|
113
|
+
[header, path, query, cookie, body].any?(&:custom_validation?)
|
|
114
|
+
end
|
|
98
115
|
end
|
|
99
116
|
|
|
100
117
|
class Location
|
|
@@ -132,6 +149,10 @@ module ActionSpec
|
|
|
132
149
|
fields.each { |field| location.add(field.copy) }
|
|
133
150
|
end
|
|
134
151
|
end
|
|
152
|
+
|
|
153
|
+
def custom_validation?
|
|
154
|
+
fields.any?(&:custom_validation?)
|
|
155
|
+
end
|
|
135
156
|
end
|
|
136
157
|
|
|
137
158
|
class Response
|