action_spec 1.0.0 → 1.2.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 +79 -65
- data/lib/action_spec/configuration.rb +1 -8
- data/lib/action_spec/doc/dsl.rb +21 -3
- data/lib/action_spec/open_api/generator.rb +1 -0
- data/lib/action_spec/railtie.rb +0 -4
- data/lib/action_spec/schema/field.rb +4 -3
- data/lib/action_spec/schema.rb +3 -2
- data/lib/action_spec/validation_result.rb +32 -9
- data/lib/action_spec/validator/runner.rb +1 -1
- data/lib/action_spec/validator.rb +2 -15
- data/lib/action_spec/version.rb +1 -1
- metadata +4 -6
- data/config/locales/en.yml +0 -6
- data/config/locales/zh.yml +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a01ddc76d37b180c1564c96963ccce0bbe32cc9efd4c29deede6e1c3e7a8f7c7
|
|
4
|
+
data.tar.gz: a97df2927f2f5df0fce593b2ff55fa123d983ab6023690326bce9634ead51b31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5ba8be1ae92c054686603867e11cda5b1af8482113ae2fcd4b36303e9631f1bdfd3c915b5e724a84dcd3c20c29f2a3f46bb92f74654716c3037625125d00820
|
|
7
|
+
data.tar.gz: 0d8327b6f04162da2ceedf697ac0a0e3dde789584870ebde8c2769ac43e6bb00836a81386b61d519b2472cd4acd8ffaf19472bbd0490c7911f9295c5216d4ed4
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Concise and Powerful API Documentation Solution for Rails.
|
|
|
6
6
|
|
|
7
7
|
- OpenAPI version: `v3.2.0`
|
|
8
8
|
- Requires: Ruby 3.1+ and Rails 7.0+
|
|
9
|
-
- Note: this project was implemented with Codex in about
|
|
9
|
+
- Note: this project was implemented with Codex in about 3 hours, has not yet been manually reviewed, and has not been validated in production. It does, however, come with fairly detailed RSpec tests generated with Codex.
|
|
10
10
|
|
|
11
11
|
## Table Of Contents
|
|
12
12
|
|
|
@@ -23,9 +23,8 @@ Concise and Powerful API Documentation Solution for Rails.
|
|
|
23
23
|
- [Type And Boundary Matrix](#type-and-boundary-matrix)
|
|
24
24
|
- [Parameter Validation And Type Coercion](#parameter-validation-and-type-coercion)
|
|
25
25
|
- [Validation Flow](#validation-flow)
|
|
26
|
-
- [Reading
|
|
26
|
+
- [Reading Processed Values With `px`](#reading-processed-values-with-px)
|
|
27
27
|
- [Errors](#errors)
|
|
28
|
-
- [Default Rescue Behavior](#default-rescue-behavior)
|
|
29
28
|
- [Configuration And I18n](#configuration-and-i18n)
|
|
30
29
|
- [Configuration](#configuration)
|
|
31
30
|
- [I18n](#i18n)
|
|
@@ -106,6 +105,7 @@ bin/rails action_spec:gen \
|
|
|
106
105
|
Notes:
|
|
107
106
|
|
|
108
107
|
- only routed controller actions with a matching `doc` declaration are included
|
|
108
|
+
- endpoints with `openapi false` are skipped even when routed
|
|
109
109
|
- Rails paths such as `/users/:id(.:format)` are rendered as `/users/{id}`
|
|
110
110
|
- parameters, request bodies, and response descriptions are generated from the current DSL support
|
|
111
111
|
- if config and environment variables do not provide `TITLE` or `VERSION`, ActionSpec falls back to application-derived defaults
|
|
@@ -150,19 +150,27 @@ end
|
|
|
150
150
|
|
|
151
151
|
```ruby
|
|
152
152
|
class ApplicationController < ActionController::API
|
|
153
|
-
doc_dry
|
|
153
|
+
doc_dry(%i[show update destroy]) {
|
|
154
154
|
path! :id, Integer
|
|
155
|
-
|
|
155
|
+
}
|
|
156
156
|
|
|
157
|
-
doc_dry
|
|
157
|
+
doc_dry(:index) {
|
|
158
158
|
query :page, Integer, default: 1
|
|
159
159
|
query :per, Integer, default: 20
|
|
160
|
-
|
|
160
|
+
}
|
|
161
161
|
end
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
All matching dry blocks are applied before the action-specific `doc`.
|
|
165
165
|
|
|
166
|
+
You can also opt an action out of OpenAPI generation from either `doc` or `doc_dry`:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
doc {
|
|
170
|
+
openapi false
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
166
174
|
### DSL Reference
|
|
167
175
|
|
|
168
176
|
#### Parameter
|
|
@@ -238,6 +246,33 @@ data :file, File
|
|
|
238
246
|
|
|
239
247
|
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.
|
|
240
248
|
|
|
249
|
+
#### OpenAPI
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
openapi false
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Use this when an action should stay out of the generated OpenAPI document. It also works inside `doc_dry`.
|
|
256
|
+
|
|
257
|
+
#### Scope
|
|
258
|
+
|
|
259
|
+
Use `scope` when you want a grouped view that spans multiple request locations:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
doc {
|
|
263
|
+
scope(:user) {
|
|
264
|
+
query :user_id, Integer
|
|
265
|
+
form data: { name: String }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Then read it from `px.scope`:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
px.scope[:user] # => { user_id: 1, name: "Tom" }
|
|
274
|
+
```
|
|
275
|
+
|
|
241
276
|
#### Response
|
|
242
277
|
|
|
243
278
|
```ruby
|
|
@@ -339,7 +374,7 @@ end
|
|
|
339
374
|
|
|
340
375
|
`User.schemas` returns a hash that can be passed directly into `form data:`, `json data:`, or `body`.
|
|
341
376
|
|
|
342
|
-
By default it includes all model fields:
|
|
377
|
+
By default, it includes all model fields:
|
|
343
378
|
|
|
344
379
|
```ruby
|
|
345
380
|
User.schemas
|
|
@@ -409,6 +444,8 @@ Example:
|
|
|
409
444
|
- DSL says `query :page, Integer`
|
|
410
445
|
- result: `px[:page] == "2"`
|
|
411
446
|
|
|
447
|
+
You can safely put this hook on a base controller. If the current action has no matching `doc`, ActionSpec skips validation and returns an empty `px`.
|
|
448
|
+
|
|
412
449
|
#### `validate_and_coerce_params!`
|
|
413
450
|
|
|
414
451
|
Validates and coerces values before exposing them on `px`.
|
|
@@ -423,36 +460,41 @@ Example:
|
|
|
423
460
|
- DSL says `query :page, Integer`
|
|
424
461
|
- result: `px[:page] == 2`
|
|
425
462
|
|
|
426
|
-
|
|
463
|
+
This hook also skips actions without a matching `doc`, so it is safe to declare on a shared base controller.
|
|
427
464
|
|
|
428
|
-
`px`
|
|
465
|
+
### Reading Processed Values With `px`
|
|
466
|
+
|
|
467
|
+
`px` stores the processed values produced by ActionSpec. With `validate_params!` they stay raw; with `validate_and_coerce_params!` they are coerced values.
|
|
429
468
|
|
|
430
469
|
```ruby
|
|
431
470
|
px[:id]
|
|
432
471
|
px[:page]
|
|
433
472
|
px[:profile][:nickname]
|
|
434
473
|
px.to_h
|
|
474
|
+
px.scope[:user]
|
|
435
475
|
```
|
|
436
476
|
|
|
437
|
-
|
|
477
|
+
Grouped views live under `px.scope`:
|
|
438
478
|
|
|
439
479
|
```ruby
|
|
440
|
-
px[:path]
|
|
441
|
-
px[:query]
|
|
442
|
-
px[:body]
|
|
443
|
-
px[:headers]
|
|
444
|
-
px[:cookies]
|
|
480
|
+
px.scope[:path]
|
|
481
|
+
px.scope[:query]
|
|
482
|
+
px.scope[:body]
|
|
483
|
+
px.scope[:headers]
|
|
484
|
+
px.scope[:cookies]
|
|
445
485
|
```
|
|
446
486
|
|
|
447
487
|
Notes:
|
|
448
488
|
|
|
449
|
-
-
|
|
489
|
+
- every declared field from path/query/body is also flattened into the top-level `px[:field]`
|
|
490
|
+
- custom `scope(:name)` buckets are also exposed through `px.scope[:name]`
|
|
491
|
+
- headers and cookies stay inside their own grouped buckets; for example, `px[:Authorization]` is not a top-level shortcut
|
|
450
492
|
- header keys are stored in lowercase dashed form, but reading remains compatible with original forms such as `Authorization` and `HTTP_AUTHORIZATION`, for example:
|
|
451
493
|
|
|
452
494
|
```ruby
|
|
453
|
-
px[:headers][:authorization]
|
|
454
|
-
px[:headers]["Authorization"]
|
|
455
|
-
px[:headers]["HTTP_AUTHORIZATION"]
|
|
495
|
+
px.scope[:headers][:authorization]
|
|
496
|
+
px.scope[:headers]["Authorization"]
|
|
497
|
+
px.scope[:headers]["HTTP_AUTHORIZATION"]
|
|
456
498
|
```
|
|
457
499
|
|
|
458
500
|
- original `params` are not mutated
|
|
@@ -461,60 +503,43 @@ px[:headers]["HTTP_AUTHORIZATION"]
|
|
|
461
503
|
|
|
462
504
|
Validation errors are stored in `ActiveModel::Errors`.
|
|
463
505
|
|
|
464
|
-
|
|
506
|
+
When validation fails, ActionSpec raises `ActionSpec::InvalidParameters`:
|
|
465
507
|
|
|
466
508
|
```ruby
|
|
467
509
|
begin
|
|
468
510
|
validate_and_coerce_params!
|
|
469
511
|
rescue ActionSpec::InvalidParameters => error
|
|
512
|
+
error.message
|
|
470
513
|
error.errors.full_messages
|
|
471
514
|
end
|
|
472
515
|
```
|
|
473
516
|
|
|
474
517
|
The exception also keeps the full validation result on `error.result` and `error.parameters`.
|
|
518
|
+
ActionSpec does not render a default error response for you, so each application can decide its own rescue and JSON format.
|
|
475
519
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
By default, when a controller raises `ActionSpec::InvalidParameters`, ActionSpec catches it automatically and returns a JSON error response:
|
|
479
|
-
|
|
480
|
-
```ruby
|
|
481
|
-
rescue_from ActionSpec::InvalidParameters
|
|
482
|
-
```
|
|
520
|
+
`error.message` is built from `error.errors.full_messages.to_sentence`, so it follows normal `ActiveModel::Errors` wording:
|
|
483
521
|
|
|
484
|
-
|
|
522
|
+
- single error: `"Page is required"`
|
|
523
|
+
- multiple errors: `"Page is required and Birthday must be a valid date"`
|
|
524
|
+
- fallback when no detailed errors are present: `"Invalid parameters"`
|
|
485
525
|
|
|
486
|
-
|
|
487
|
-
{
|
|
488
|
-
"errors": {
|
|
489
|
-
"page": ["Page is required"]
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
```
|
|
526
|
+
Use `error.errors` when you need structured details, and `error.message` when you only need a single summary string.
|
|
493
527
|
|
|
494
528
|
## Configuration And I18n
|
|
495
529
|
|
|
496
530
|
### Configuration
|
|
497
531
|
|
|
498
532
|
```ruby
|
|
499
|
-
ActionSpec.configure
|
|
500
|
-
config.rescue_invalid_parameters = true
|
|
501
|
-
config.invalid_parameters_status = :bad_request
|
|
533
|
+
ActionSpec.configure { |config|
|
|
502
534
|
config.open_api_output = "docs/openapi.yml"
|
|
503
535
|
config.open_api_title = "My API"
|
|
504
536
|
config.open_api_version = "2026.03"
|
|
505
537
|
config.open_api_server_url = "https://api.example.com"
|
|
506
538
|
|
|
507
|
-
config.error_messages[:invalid_type] = ->(_attribute, options)
|
|
539
|
+
config.error_messages[:invalid_type] = ->(_attribute, options) {
|
|
508
540
|
"should be coercible to #{options.fetch(:expected)}"
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
config.invalid_parameters_renderer = ->(controller, error) do
|
|
512
|
-
controller.render json: {
|
|
513
|
-
code: "invalid_parameters",
|
|
514
|
-
errors: error.errors.to_hash(full_messages: true)
|
|
515
|
-
}, status: :unprocessable_entity
|
|
516
|
-
end
|
|
517
|
-
end
|
|
541
|
+
}
|
|
542
|
+
}
|
|
518
543
|
```
|
|
519
544
|
|
|
520
545
|
Available config keys:
|
|
@@ -523,18 +548,6 @@ Available config keys:
|
|
|
523
548
|
Default: `ActionSpec::InvalidParameters`.
|
|
524
549
|
Controls which exception class is raised when validation fails.
|
|
525
550
|
|
|
526
|
-
- `invalid_parameters_status`
|
|
527
|
-
Default: `:bad_request`.
|
|
528
|
-
Controls the HTTP status used by the built-in `rescue_from` renderer.
|
|
529
|
-
|
|
530
|
-
- `rescue_invalid_parameters`
|
|
531
|
-
Default: `true`.
|
|
532
|
-
When this option is enabled, controllers use the default `rescue_from ActionSpec::InvalidParameters`.
|
|
533
|
-
|
|
534
|
-
- `invalid_parameters_renderer`
|
|
535
|
-
Default: `nil`.
|
|
536
|
-
Lets you replace the built-in JSON error response. It can be a proc receiving `(controller, error)`, or a block executed in controller context.
|
|
537
|
-
|
|
538
551
|
- `error_messages`
|
|
539
552
|
Default: `{}`.
|
|
540
553
|
Lets you override error messages by error type, or by attribute plus error type.
|
|
@@ -557,7 +570,7 @@ Available config keys:
|
|
|
557
570
|
|
|
558
571
|
### I18n
|
|
559
572
|
|
|
560
|
-
ActionSpec
|
|
573
|
+
ActionSpec uses `ActiveModel::Errors`, so you can override both messages and attribute names:
|
|
561
574
|
|
|
562
575
|
```yml
|
|
563
576
|
en:
|
|
@@ -574,13 +587,13 @@ en:
|
|
|
574
587
|
You can also override messages per error type or per attribute in Ruby:
|
|
575
588
|
|
|
576
589
|
```ruby
|
|
577
|
-
ActionSpec.configure
|
|
590
|
+
ActionSpec.configure { |config|
|
|
578
591
|
config.error_messages[:required] = "must be present"
|
|
579
592
|
config.error_messages[:invalid_type] = ->(_attribute, options) { "must be a valid #{options.fetch(:expected)}" }
|
|
580
593
|
config.error_messages[:page] = {
|
|
581
594
|
required: "page is mandatory"
|
|
582
595
|
}
|
|
583
|
-
|
|
596
|
+
}
|
|
584
597
|
```
|
|
585
598
|
|
|
586
599
|
## What Is Not Implemented Yet
|
|
@@ -605,7 +618,8 @@ end
|
|
|
605
618
|
- richer schema keywords beyond the current subset, including nullable/blank semantics, object-level constraints, and composition keywords such as `oneOf`, `anyOf`, `allOf`, and `not`
|
|
606
619
|
|
|
607
620
|
## Contributing
|
|
608
|
-
|
|
621
|
+
|
|
622
|
+
Contributions / Issues are welcome.
|
|
609
623
|
|
|
610
624
|
## License
|
|
611
625
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module ActionSpec
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :invalid_parameters_exception_class, :
|
|
6
|
-
:invalid_parameters_renderer, :open_api_output, :open_api_title, :open_api_version,
|
|
5
|
+
attr_accessor :invalid_parameters_exception_class, :open_api_output, :open_api_title, :open_api_version,
|
|
7
6
|
:open_api_server_url
|
|
8
7
|
attr_reader :error_messages
|
|
9
8
|
|
|
10
9
|
def initialize
|
|
11
10
|
@invalid_parameters_exception_class = ActionSpec::InvalidParameters
|
|
12
|
-
@invalid_parameters_status = :bad_request
|
|
13
|
-
@rescue_invalid_parameters = true
|
|
14
|
-
@invalid_parameters_renderer = nil
|
|
15
11
|
@open_api_output = "docs/openapi.yml"
|
|
16
12
|
@open_api_title = nil
|
|
17
13
|
@open_api_version = nil
|
|
@@ -33,9 +29,6 @@ module ActionSpec
|
|
|
33
29
|
def dup
|
|
34
30
|
self.class.new.tap do |copy|
|
|
35
31
|
copy.invalid_parameters_exception_class = invalid_parameters_exception_class
|
|
36
|
-
copy.invalid_parameters_status = invalid_parameters_status
|
|
37
|
-
copy.rescue_invalid_parameters = rescue_invalid_parameters
|
|
38
|
-
copy.invalid_parameters_renderer = invalid_parameters_renderer
|
|
39
32
|
copy.open_api_output = open_api_output
|
|
40
33
|
copy.open_api_title = open_api_title
|
|
41
34
|
copy.open_api_version = open_api_version
|
data/lib/action_spec/doc/dsl.rb
CHANGED
|
@@ -7,6 +7,7 @@ module ActionSpec
|
|
|
7
7
|
|
|
8
8
|
def initialize(endpoint)
|
|
9
9
|
@endpoint = endpoint
|
|
10
|
+
@scopes = []
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
PARAM_LOCATIONS.each do |location_name|
|
|
@@ -55,6 +56,17 @@ module ActionSpec
|
|
|
55
56
|
add_body(:form, { name => options.merge(type:) })
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
def scope(name, &block)
|
|
60
|
+
scopes.push(name.to_sym)
|
|
61
|
+
instance_exec(&block)
|
|
62
|
+
ensure
|
|
63
|
+
scopes.pop
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def openapi(enabled)
|
|
67
|
+
endpoint.options[:openapi] = enabled
|
|
68
|
+
end
|
|
69
|
+
|
|
58
70
|
def response(code, description = nil, media_type = nil, desc: nil, **options)
|
|
59
71
|
endpoint.add_response(
|
|
60
72
|
code,
|
|
@@ -73,10 +85,11 @@ module ActionSpec
|
|
|
73
85
|
private
|
|
74
86
|
|
|
75
87
|
attr_reader :endpoint
|
|
88
|
+
attr_reader :scopes
|
|
76
89
|
|
|
77
90
|
def add_param(location_name, name, type, required:, **options)
|
|
78
91
|
schema = ActionSpec::Schema.build(type, **options)
|
|
79
|
-
endpoint.request.add_param(location_name, ActionSpec::Schema::Field.new(name:, required:, schema:))
|
|
92
|
+
endpoint.request.add_param(location_name, ActionSpec::Schema::Field.new(name:, required:, schema:, scopes: scopes.dup))
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
def add_many(location_name, params, required:)
|
|
@@ -86,7 +99,12 @@ module ActionSpec
|
|
|
86
99
|
if (schema_options.keys - ActionSpec::Schema::OPTION_KEYS).present?
|
|
87
100
|
endpoint.request.add_param(
|
|
88
101
|
location_name,
|
|
89
|
-
ActionSpec::Schema::Field.new(
|
|
102
|
+
ActionSpec::Schema::Field.new(
|
|
103
|
+
name:,
|
|
104
|
+
required:,
|
|
105
|
+
schema: ActionSpec::Schema.from_definition(definition),
|
|
106
|
+
scopes: scopes.dup
|
|
107
|
+
)
|
|
90
108
|
)
|
|
91
109
|
else
|
|
92
110
|
add_param(location_name, name, String, required:, **definition)
|
|
@@ -100,7 +118,7 @@ module ActionSpec
|
|
|
100
118
|
end
|
|
101
119
|
|
|
102
120
|
def add_body(media_type, definition)
|
|
103
|
-
ActionSpec::Schema.build_fields(definition).each_value do |field|
|
|
121
|
+
ActionSpec::Schema.build_fields(definition, scopes: scopes.dup).each_value do |field|
|
|
104
122
|
endpoint.request.add_body(media_type, field)
|
|
105
123
|
end
|
|
106
124
|
end
|
|
@@ -48,6 +48,7 @@ module ActionSpec
|
|
|
48
48
|
next unless (controller = controller_for(route))
|
|
49
49
|
next unless controller.respond_to?(:action_spec_for)
|
|
50
50
|
next unless (endpoint = controller.action_spec_for(route_action(route)))
|
|
51
|
+
next if endpoint.options[:openapi] == false
|
|
51
52
|
|
|
52
53
|
path = normalized_path(route)
|
|
53
54
|
next if path.blank?
|
data/lib/action_spec/railtie.rb
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
module ActionSpec
|
|
2
2
|
class Railtie < ::Rails::Railtie
|
|
3
|
-
initializer "action_spec.i18n" do |app|
|
|
4
|
-
app.config.i18n.load_path += Dir[root.join("config/locales/*.yml")]
|
|
5
|
-
end
|
|
6
|
-
|
|
7
3
|
initializer "action_spec.controller" do
|
|
8
4
|
ActiveSupport.on_load(:action_controller_base) do
|
|
9
5
|
include ActionSpec::Doc
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
module ActionSpec
|
|
4
4
|
module Schema
|
|
5
5
|
class Field
|
|
6
|
-
attr_reader :name, :schema
|
|
6
|
+
attr_reader :name, :schema, :scopes
|
|
7
7
|
|
|
8
|
-
def initialize(name:, required:, schema:)
|
|
8
|
+
def initialize(name:, required:, schema:, scopes: [])
|
|
9
9
|
@name = name.to_sym
|
|
10
10
|
@required = required
|
|
11
11
|
@schema = schema
|
|
12
|
+
@scopes = Array(scopes).map(&:to_sym).freeze
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def required?
|
|
@@ -20,7 +21,7 @@ module ActionSpec
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def copy
|
|
23
|
-
self.class.new(name:, required: required?, schema: schema.copy)
|
|
24
|
+
self.class.new(name:, required: required?, schema: schema.copy, scopes:)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
data/lib/action_spec/schema.rb
CHANGED
|
@@ -40,13 +40,14 @@ module ActionSpec
|
|
|
40
40
|
ObjectOf.new(build_fields(definition))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def build_fields(definition_hash)
|
|
43
|
+
def build_fields(definition_hash, scopes: [])
|
|
44
44
|
definition_hash.each_with_object(ActiveSupport::OrderedHash.new) do |(name, definition), fields|
|
|
45
45
|
schema = build_field_schema(definition)
|
|
46
46
|
fields[field_name(name)] = Field.new(
|
|
47
47
|
name: field_name(name),
|
|
48
48
|
required: required_key?(name),
|
|
49
|
-
schema
|
|
49
|
+
schema:,
|
|
50
|
+
scopes:
|
|
50
51
|
)
|
|
51
52
|
end
|
|
52
53
|
end
|
|
@@ -5,26 +5,29 @@ module ActionSpec
|
|
|
5
5
|
extend ActiveModel::Naming
|
|
6
6
|
extend ActiveModel::Translation
|
|
7
7
|
|
|
8
|
+
BUILT_IN_SCOPES = %i[path query body headers cookies].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def empty_px
|
|
12
|
+
new.px
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
attr_reader :errors, :px
|
|
9
17
|
|
|
10
18
|
def initialize
|
|
11
19
|
@errors = ActiveModel::Errors.new(self)
|
|
12
|
-
@px =
|
|
13
|
-
path: ActiveSupport::HashWithIndifferentAccess.new,
|
|
14
|
-
query: ActiveSupport::HashWithIndifferentAccess.new,
|
|
15
|
-
body: ActiveSupport::HashWithIndifferentAccess.new,
|
|
16
|
-
headers: HeaderHash.new,
|
|
17
|
-
cookies: ActiveSupport::HashWithIndifferentAccess.new
|
|
18
|
-
)
|
|
20
|
+
@px = build_px
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def invalid?
|
|
22
24
|
errors.any?
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def assign(location, key, value)
|
|
27
|
+
def assign(location, key, value, scopes: [])
|
|
26
28
|
bucket(location)[key] = value
|
|
27
29
|
px[key] = value if root_bucket?(location)
|
|
30
|
+
Array(scopes).each { |scope_name| scope_bucket(scope_name)[key] = value }
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def add_error(attribute, type, **options)
|
|
@@ -60,8 +63,28 @@ module ActionSpec
|
|
|
60
63
|
|
|
61
64
|
private
|
|
62
65
|
|
|
66
|
+
def build_px
|
|
67
|
+
values = ActiveSupport::HashWithIndifferentAccess.new
|
|
68
|
+
scope = ActiveSupport::HashWithIndifferentAccess.new
|
|
69
|
+
|
|
70
|
+
BUILT_IN_SCOPES.each do |scope_name|
|
|
71
|
+
bucket = scope_name == :headers ? HeaderHash.new : ActiveSupport::HashWithIndifferentAccess.new
|
|
72
|
+
values[scope_name] = bucket
|
|
73
|
+
scope[scope_name] = bucket
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Keep px hash-like while exposing grouped views through px.scope.
|
|
77
|
+
values.instance_variable_set(:@scope, scope)
|
|
78
|
+
values.define_singleton_method(:scope) { @scope }
|
|
79
|
+
values
|
|
80
|
+
end
|
|
81
|
+
|
|
63
82
|
def bucket(location)
|
|
64
|
-
px.fetch(location)
|
|
83
|
+
px.scope.fetch(location)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def scope_bucket(name)
|
|
87
|
+
px.scope[name] ||= ActiveSupport::HashWithIndifferentAccess.new
|
|
65
88
|
end
|
|
66
89
|
|
|
67
90
|
def root_bucket?(location)
|
|
@@ -28,7 +28,7 @@ module ActionSpec
|
|
|
28
28
|
value = resolve_field(field, result:, source:, location:)
|
|
29
29
|
next if value.equal?(ActionSpec::Schema::Missing)
|
|
30
30
|
|
|
31
|
-
result.assign(location, storage_key(field, location), value)
|
|
31
|
+
result.assign(location, storage_key(field, location), value, scopes: field.scopes)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -6,12 +6,8 @@ module ActionSpec
|
|
|
6
6
|
module Validator
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
|
-
included do
|
|
10
|
-
rescue_from ActionSpec::InvalidParameters, with: :render_invalid_parameters if ActionSpec.config.rescue_invalid_parameters
|
|
11
|
-
end
|
|
12
|
-
|
|
13
9
|
def px
|
|
14
|
-
@px ||=
|
|
10
|
+
@px ||= ValidationResult.empty_px
|
|
15
11
|
end
|
|
16
12
|
|
|
17
13
|
def validate_params!
|
|
@@ -26,21 +22,12 @@ module ActionSpec
|
|
|
26
22
|
|
|
27
23
|
def validate_with(coerce:)
|
|
28
24
|
endpoint = self.class.respond_to?(:action_spec_for) ? self.class.action_spec_for(action_name) : nil
|
|
29
|
-
return
|
|
25
|
+
return ValidationResult.empty_px unless endpoint
|
|
30
26
|
|
|
31
27
|
result = Runner.new(endpoint:, controller: self, coerce:).call
|
|
32
28
|
raise ActionSpec.config.invalid_parameters_exception_class.new(result) if result.invalid?
|
|
33
29
|
|
|
34
30
|
result.px
|
|
35
31
|
end
|
|
36
|
-
|
|
37
|
-
def render_invalid_parameters(error)
|
|
38
|
-
if (renderer = ActionSpec.config.invalid_parameters_renderer)
|
|
39
|
-
return renderer.arity == 2 ? renderer.call(self, error) : instance_exec(error, &renderer)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
render json: { errors: error.errors.to_hash(full_messages: true) },
|
|
43
|
-
status: ActionSpec.config.invalid_parameters_status
|
|
44
|
-
end
|
|
45
32
|
end
|
|
46
33
|
end
|
data/lib/action_spec/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: action_spec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zhandao
|
|
@@ -43,7 +43,7 @@ dependencies:
|
|
|
43
43
|
- - "<"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '9.0'
|
|
46
|
-
description:
|
|
46
|
+
description: A concise Rails DSL for declaring API request and response schemas.
|
|
47
47
|
email:
|
|
48
48
|
- a@skipping.cat
|
|
49
49
|
executables: []
|
|
@@ -53,8 +53,6 @@ files:
|
|
|
53
53
|
- MIT-LICENSE
|
|
54
54
|
- README.md
|
|
55
55
|
- Rakefile
|
|
56
|
-
- config/locales/en.yml
|
|
57
|
-
- config/locales/zh.yml
|
|
58
56
|
- lib/action_spec.rb
|
|
59
57
|
- lib/action_spec/configuration.rb
|
|
60
58
|
- lib/action_spec/doc.rb
|
|
@@ -87,8 +85,8 @@ licenses:
|
|
|
87
85
|
- MIT
|
|
88
86
|
metadata:
|
|
89
87
|
homepage_uri: https://github.com/action-spec/action_spec
|
|
90
|
-
source_code_uri: https://github.com/action-spec/action_spec
|
|
91
|
-
changelog_uri: https://github.com/action-spec/action_spec/
|
|
88
|
+
source_code_uri: https://github.com/action-spec/action_spec/tree/main
|
|
89
|
+
changelog_uri: https://github.com/action-spec/action_spec/releases
|
|
92
90
|
rdoc_options: []
|
|
93
91
|
require_paths:
|
|
94
92
|
- lib
|
data/config/locales/en.yml
DELETED