action_spec 1.2.0 → 1.4.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 +125 -79
- data/lib/action_spec/configuration.rb +3 -1
- data/lib/action_spec/doc/dsl.rb +59 -31
- data/lib/action_spec/doc/endpoint.rb +87 -6
- data/lib/action_spec/doc.rb +14 -7
- data/lib/action_spec/open_api/generator.rb +19 -2
- data/lib/action_spec/open_api/operation.rb +22 -3
- data/lib/action_spec/open_api/schema.rb +141 -4
- data/lib/action_spec/railtie.rb +4 -0
- data/lib/action_spec/schema/array_of.rb +1 -1
- data/lib/action_spec/schema/base.rb +9 -4
- data/lib/action_spec/schema/field.rb +44 -3
- data/lib/action_spec/schema/object_of.rb +2 -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 +68 -11
- data/lib/action_spec/validator/runner.rb +17 -1
- data/lib/action_spec/version.rb +1 -1
- data/lib/tasks/action_spec_tasks.rake +4 -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: 432c84ac7303d7194f5617f2fe724bdb2d332662eb543a0545712731cac767dc
|
|
4
|
+
data.tar.gz: 90343cbda95d9a89452aabf86da6731606d2404a4c2bcd0a9358b48f705bd6cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 631a11eea99a092b2c104ff3934fcddd1777f8a2346468ef91170ff1bc5acea9844002b438b4ec668a5a20f7a97d58c69bdf8f99482b1b757e4ccf01fadf119c
|
|
7
|
+
data.tar.gz: 886a2975a48967efcf844c4577f05c2ebbb4cd8ed2d6c883e80c6830b1388f57b38b9f6e202376b01a71d1109ec75869d20ab143950d60cb5638e98e4b111c02
|
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,24 +10,30 @@ 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
|
-
|
|
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. [Errors](#errors)
|
|
33
|
+
5. [Configuration And I18n](#configuration-and-i18n)
|
|
34
|
+
1. [Configuration](#configuration)
|
|
35
|
+
2. [I18n](#i18n)
|
|
36
|
+
6. [AI Generation Style Guide](#ai-generation-style-guide)
|
|
31
37
|
|
|
32
38
|
## Example
|
|
33
39
|
|
|
@@ -56,10 +62,8 @@ class UsersController < ApplicationController
|
|
|
56
62
|
}
|
|
57
63
|
def create
|
|
58
64
|
User.create!(
|
|
59
|
-
account_id: px[:account_id],
|
|
60
|
-
|
|
61
|
-
birthday: px[:birthday],
|
|
62
|
-
admin: px[:admin]
|
|
65
|
+
account_id: px[:account_id], name: px[:name],
|
|
66
|
+
**px.slice(:birthday, :admin, :profile)
|
|
63
67
|
)
|
|
64
68
|
end
|
|
65
69
|
end
|
|
@@ -92,7 +96,7 @@ By default, this writes to:
|
|
|
92
96
|
docs/openapi.yml
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
Environment variables can override the default output path and document metadata:
|
|
96
100
|
|
|
97
101
|
```bash
|
|
98
102
|
bin/rails action_spec:gen \
|
|
@@ -146,6 +150,18 @@ def create
|
|
|
146
150
|
end
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
Override the default OpenAPI tag with `tag:`. By default, the tag comes from the routed `controller_path`:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
doc_dry(:index, tag: "backoffice")
|
|
157
|
+
|
|
158
|
+
doc("List users", tag: "members") {
|
|
159
|
+
query :status, String
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Generated OpenAPI operations also include an `operationId`, built from the final tag plus the action name, for example `members_index` or `users_create`.
|
|
164
|
+
|
|
149
165
|
### `doc_dry`
|
|
150
166
|
|
|
151
167
|
```ruby
|
|
@@ -163,15 +179,7 @@ end
|
|
|
163
179
|
|
|
164
180
|
All matching dry blocks are applied before the action-specific `doc`.
|
|
165
181
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
```ruby
|
|
169
|
-
doc {
|
|
170
|
-
openapi false
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### DSL Reference
|
|
182
|
+
### DSL Inside `doc`
|
|
175
183
|
|
|
176
184
|
#### Parameter
|
|
177
185
|
|
|
@@ -189,7 +197,16 @@ cookie :remember_token, String
|
|
|
189
197
|
cookie! :remember_token, String
|
|
190
198
|
```
|
|
191
199
|
|
|
192
|
-
Bang methods mark the field as required. For example, `query! :page, Integer` means the request must include `page`.
|
|
200
|
+
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
|
+
|
|
202
|
+
If you prefer not to use bang methods, you can also write `required: true`:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
query :page, Integer, required: true
|
|
206
|
+
json data: {
|
|
207
|
+
title: { type: String, required: true }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
193
210
|
|
|
194
211
|
Batch declaration forms:
|
|
195
212
|
|
|
@@ -230,11 +247,9 @@ Convenience helpers:
|
|
|
230
247
|
|
|
231
248
|
```ruby
|
|
232
249
|
json data: { name!: String }
|
|
233
|
-
|
|
234
250
|
json! data: { name!: String }
|
|
235
251
|
|
|
236
252
|
form data: { file!: File, position: String }
|
|
237
|
-
|
|
238
253
|
form! data: { file!: File }
|
|
239
254
|
```
|
|
240
255
|
|
|
@@ -244,16 +259,23 @@ Single multipart field helper:
|
|
|
244
259
|
data :file, File
|
|
245
260
|
```
|
|
246
261
|
|
|
247
|
-
|
|
262
|
+
Notes:
|
|
263
|
+
|
|
264
|
+
1. When multiple `body/body!`, `json/json!`, or `form/form!` declarations are used:
|
|
265
|
+
- declarations with the same media type are merged
|
|
266
|
+
- if multiple media types are declared, the generated OpenAPI document will emit multiple media types
|
|
267
|
+
- field validation and coercion do not distinguish between media types, and always read values from Rails `params`
|
|
268
|
+
|
|
269
|
+
`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.
|
|
248
270
|
|
|
249
|
-
####
|
|
271
|
+
#### `openapi false`
|
|
272
|
+
|
|
273
|
+
You can also opt an action out of OpenAPI generation from either `doc` or `doc_dry`:
|
|
250
274
|
|
|
251
275
|
```ruby
|
|
252
276
|
openapi false
|
|
253
277
|
```
|
|
254
278
|
|
|
255
|
-
Use this when an action should stay out of the generated OpenAPI document. It also works inside `doc_dry`.
|
|
256
|
-
|
|
257
279
|
#### Scope
|
|
258
280
|
|
|
259
281
|
Use `scope` when you want a grouped view that spans multiple request locations:
|
|
@@ -264,6 +286,7 @@ doc {
|
|
|
264
286
|
query :user_id, Integer
|
|
265
287
|
form data: { name: String }
|
|
266
288
|
}
|
|
289
|
+
form data: { not_in_scope: String }
|
|
267
290
|
}
|
|
268
291
|
```
|
|
269
292
|
|
|
@@ -278,11 +301,27 @@ px.scope[:user] # => { user_id: 1, name: "Tom" }
|
|
|
278
301
|
```ruby
|
|
279
302
|
response 200, desc: "success"
|
|
280
303
|
response 422, "validation failed"
|
|
281
|
-
|
|
304
|
+
response 200, :json, data: { code!: Integer, result: Object }
|
|
305
|
+
|
|
282
306
|
error 401, "unauthorized"
|
|
307
|
+
error 503, { code!: Integer, message!: String } # error data schema
|
|
308
|
+
error 503, { code: 1000, message: "invalid params" } # unnamed error example
|
|
309
|
+
error 503, invalid_params: { code: 1000, message: "invalid params" } # named error example
|
|
310
|
+
# declare multiple named examples in batch
|
|
311
|
+
errors 503, {
|
|
312
|
+
invalid_params: { code: 1000, message: "invalid params" },
|
|
313
|
+
network_error: { code: 1001, message: "network error" }
|
|
314
|
+
}
|
|
315
|
+
errors 503, network_error: { code: 1001 }, upstream_timeout: { code: 1002 } # braces are also optional
|
|
316
|
+
|
|
283
317
|
```
|
|
284
318
|
|
|
285
|
-
Response declarations are stored as metadata
|
|
319
|
+
Response declarations are stored as metadata and are emitted in OpenAPI. They do not render responses automatically at runtime.
|
|
320
|
+
|
|
321
|
+
Notes:
|
|
322
|
+
|
|
323
|
+
1. `response`, `error`, and `errors` default `media_type` to `:json` and this default is configurable.
|
|
324
|
+
2. If examples are declared without an explicit schema, ActionSpec infers the response schema from the example payloads for OpenAPI generation.
|
|
286
325
|
|
|
287
326
|
## Schemas
|
|
288
327
|
|
|
@@ -305,7 +344,11 @@ Meaning of `!`:
|
|
|
305
344
|
|
|
306
345
|
- `query!`, `path!`, `header!`, `cookie!` mark the parameter itself as required
|
|
307
346
|
- keys such as `name!:` or `nickname!:` mark nested object fields as required
|
|
308
|
-
- `body!`, `json!`, and `form!`
|
|
347
|
+
- `body!`, `json!`, and `form!` mark the root request body as required
|
|
348
|
+
|
|
349
|
+
You can also use `required: true` instead of bang syntax for parameters, nested fields, and the root request body.
|
|
350
|
+
|
|
351
|
+
`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`.
|
|
309
352
|
|
|
310
353
|
#### Field Types
|
|
311
354
|
|
|
@@ -337,26 +380,31 @@ json data: {
|
|
|
337
380
|
|
|
338
381
|
#### Field Options
|
|
339
382
|
|
|
340
|
-
These options are currently used by the validator:
|
|
341
|
-
|
|
342
383
|
```ruby
|
|
343
384
|
query :page, Integer, default: 1
|
|
344
385
|
query :today, Date, default: -> { Time.current.to_date }
|
|
345
386
|
query :status, String, enum: %w[draft published]
|
|
346
387
|
query :score, Integer, range: { ge: 1, le: 5 }
|
|
347
388
|
query :slug, String, pattern: /\A[a-z\-]+\z/
|
|
389
|
+
query :title, String, blank: false # or allow_blank: false
|
|
390
|
+
|
|
391
|
+
query :nickname, String, transform: :downcase
|
|
392
|
+
query :page, Integer, transform: -> { it + 1 }, px: :page_number
|
|
393
|
+
query :request_id, String, px_key: :trace_id
|
|
348
394
|
```
|
|
349
395
|
|
|
350
|
-
|
|
396
|
+
Notes:
|
|
351
397
|
|
|
352
|
-
- `
|
|
353
|
-
- `
|
|
354
|
-
- `examples`
|
|
398
|
+
- `transform` accepts a `Symbol` or a `Proc` and runs **after coercion**, before the value is written into `px`
|
|
399
|
+
- `px` and `px_key` customize the key name written into `px`; `px` is the short form of `px_key`
|
|
355
400
|
|
|
356
|
-
These options are
|
|
401
|
+
These options are used by OpenAPI generation:
|
|
357
402
|
|
|
358
|
-
|
|
359
|
-
|
|
403
|
+
```ruby
|
|
404
|
+
query :page, Integer, desc: "page number", example: 1, examples: [1, 2, 3]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
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.
|
|
360
408
|
|
|
361
409
|
#### Schemas From ActiveRecord
|
|
362
410
|
|
|
@@ -466,6 +514,8 @@ This hook also skips actions without a matching `doc`, so it is safe to declare
|
|
|
466
514
|
|
|
467
515
|
`px` stores the processed values produced by ActionSpec. With `validate_params!` they stay raw; with `validate_and_coerce_params!` they are coerced values.
|
|
468
516
|
|
|
517
|
+
Because `px` is still a hash, you can also use helpers such as `px.slice(...)` to simplify parameter access code.
|
|
518
|
+
|
|
469
519
|
```ruby
|
|
470
520
|
px[:id]
|
|
471
521
|
px[:page]
|
|
@@ -535,6 +585,7 @@ ActionSpec.configure { |config|
|
|
|
535
585
|
config.open_api_title = "My API"
|
|
536
586
|
config.open_api_version = "2026.03"
|
|
537
587
|
config.open_api_server_url = "https://api.example.com"
|
|
588
|
+
config.default_response_media_type = :json
|
|
538
589
|
|
|
539
590
|
config.error_messages[:invalid_type] = ->(_attribute, options) {
|
|
540
591
|
"should be coercible to #{options.fetch(:expected)}"
|
|
@@ -544,29 +595,13 @@ ActionSpec.configure { |config|
|
|
|
544
595
|
|
|
545
596
|
Available config keys:
|
|
546
597
|
|
|
547
|
-
- `invalid_parameters_exception_class`
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
- `
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
- `open_api_output`
|
|
556
|
-
Default: `"docs/openapi.yml"`.
|
|
557
|
-
Controls where `bin/rails action_spec:gen` writes the generated OpenAPI document.
|
|
558
|
-
|
|
559
|
-
- `open_api_title`
|
|
560
|
-
Default: `nil`.
|
|
561
|
-
Sets the default OpenAPI `info.title` used by `bin/rails action_spec:gen`.
|
|
562
|
-
|
|
563
|
-
- `open_api_version`
|
|
564
|
-
Default: `nil`.
|
|
565
|
-
Sets the default OpenAPI `info.version` used by `bin/rails action_spec:gen`.
|
|
566
|
-
|
|
567
|
-
- `open_api_server_url`
|
|
568
|
-
Default: `nil`.
|
|
569
|
-
Sets the default server URL emitted in the generated OpenAPI document.
|
|
598
|
+
- `invalid_parameters_exception_class`: Default `ActionSpec::InvalidParameters`; controls which exception class is raised when validation fails.
|
|
599
|
+
- `error_messages`: Default `{}`; lets you override error messages by error type, or by attribute plus error type.
|
|
600
|
+
- `open_api_output`: Default `"docs/openapi.yml"`; controls where `bin/rails action_spec:gen` writes the generated OpenAPI document.
|
|
601
|
+
- `open_api_title`: Default `nil`; sets the default OpenAPI `info.title` used by `bin/rails action_spec:gen`.
|
|
602
|
+
- `open_api_version`: Default `nil`; sets the default OpenAPI `info.version` used by `bin/rails action_spec:gen`.
|
|
603
|
+
- `open_api_server_url`: Default `nil`; sets the default server URL emitted in the generated OpenAPI document.
|
|
604
|
+
- `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.
|
|
570
605
|
|
|
571
606
|
### I18n
|
|
572
607
|
|
|
@@ -596,15 +631,26 @@ ActionSpec.configure { |config|
|
|
|
596
631
|
}
|
|
597
632
|
```
|
|
598
633
|
|
|
634
|
+
## AI Generation Style Guide
|
|
635
|
+
|
|
636
|
+
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:
|
|
637
|
+
|
|
638
|
+
- use `doc { }` or `doc("Summary") { }`; do not add the action name, and do not leave a blank line between the `doc` block and the action method
|
|
639
|
+
- use `{ }` blocks inside `doc` as well; prefer them over `do ... end`
|
|
640
|
+
- when a batch has 3 fields or fewer and does not contain nested hashes, prefer a single-line style, for example:
|
|
641
|
+
- `json data: { type: String, required: true }`
|
|
642
|
+
- `in_query(name: String, value: String)` (prefer `in_xxx(...)` batch declarations over multiple `xx` DSL lines when possible)
|
|
643
|
+
- use `doc_dry`, `scope`, and `transform`、`px(px_key)`、`px.slice` to keep controller concise
|
|
644
|
+
- when request parameters match model declarations, prefer `.schemas` to keep `doc` concise
|
|
645
|
+
|
|
599
646
|
## What Is Not Implemented Yet
|
|
600
647
|
|
|
601
648
|
- reusable `components` generation
|
|
602
649
|
- `$ref` generation and deduplication
|
|
603
|
-
- `description`, `
|
|
650
|
+
- `description`, `externalDocs`, `deprecated`, and `security` on operations
|
|
604
651
|
- parameter-level `style`, `explode`, `allowReserved`, `examples`, and richer header/cookie serialization controls
|
|
605
652
|
- request body `encoding`
|
|
606
653
|
- multiple request/response media types beyond the current direct DSL mapping
|
|
607
|
-
- response body schema generation; current `response` / `resp` / `error` declarations only generate response descriptions
|
|
608
654
|
- response headers
|
|
609
655
|
- response links
|
|
610
656
|
- callbacks
|
|
@@ -615,7 +661,7 @@ ActionSpec.configure { |config|
|
|
|
615
661
|
- top-level `tags`
|
|
616
662
|
- top-level `externalDocs`
|
|
617
663
|
- `jsonSchemaDialect`
|
|
618
|
-
- richer schema keywords beyond the current subset, including
|
|
664
|
+
- richer schema keywords beyond the current subset, including object-level constraints, and composition keywords such as `oneOf`, `anyOf`, `allOf`, and `not`
|
|
619
665
|
|
|
620
666
|
## Contributing
|
|
621
667
|
|
|
@@ -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
|
data/lib/action_spec/doc/dsl.rb
CHANGED
|
@@ -28,12 +28,12 @@ module ActionSpec
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def body(media_type, data: {}, **)
|
|
32
|
-
add_body(media_type, data)
|
|
31
|
+
def body(media_type, data: {}, required: false, **)
|
|
32
|
+
add_body(media_type, data, required:)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def body!(media_type, data: {}, **)
|
|
36
|
-
add_body(media_type, data)
|
|
36
|
+
add_body(media_type, data, required: true)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def json(data:, **options)
|
|
@@ -67,57 +67,85 @@ module ActionSpec
|
|
|
67
67
|
endpoint.options[:openapi] = enabled
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
RESPONSE_MEDIA_TYPES = %i[json form].freeze
|
|
71
|
+
|
|
72
|
+
def response(code, description = nil, media_type = nil, desc: nil, data: nil, example: nil, examples: nil, **options)
|
|
73
|
+
description, media_type = normalize_response_arguments(description, media_type)
|
|
74
|
+
schema, example, examples = normalize_response_body(description, data:, example:, examples:, options:)
|
|
71
75
|
endpoint.add_response(
|
|
72
76
|
code,
|
|
73
77
|
Response.new(
|
|
74
78
|
code:,
|
|
75
|
-
description: description
|
|
76
|
-
media_type
|
|
79
|
+
description: resolved_description(description, desc),
|
|
80
|
+
media_type: media_type || ActionSpec.config.default_response_media_type,
|
|
81
|
+
schema:,
|
|
82
|
+
example:,
|
|
83
|
+
examples:,
|
|
77
84
|
options:
|
|
78
85
|
)
|
|
79
86
|
)
|
|
80
87
|
end
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
def error(code, description = nil, media_type = nil, desc: nil, **options)
|
|
90
|
+
response(code, description, media_type, desc: desc || "Error", **options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def errors(code, examples = nil, media_type = nil, desc: "Error", **options)
|
|
94
|
+
response(code, nil, media_type, desc:, examples: examples || options, **options.except(*Array((examples || options).keys)))
|
|
95
|
+
end
|
|
84
96
|
|
|
85
97
|
private
|
|
86
98
|
|
|
87
99
|
attr_reader :endpoint
|
|
88
100
|
attr_reader :scopes
|
|
89
101
|
|
|
102
|
+
def normalize_response_arguments(description, media_type)
|
|
103
|
+
return [nil, description] if media_type.nil? && response_media_type?(description)
|
|
104
|
+
|
|
105
|
+
[description, media_type]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def response_media_type?(value)
|
|
109
|
+
value.is_a?(Symbol) && RESPONSE_MEDIA_TYPES.include?(value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_response_body(description, data:, example:, examples:, options:)
|
|
113
|
+
return [data && ActionSpec::Schema.from_definition(data), example, examples] if data || example || examples
|
|
114
|
+
return [nil, nil, options.presence] if options.present?
|
|
115
|
+
return [nil, nil, nil] if description.is_a?(String) || description.nil?
|
|
116
|
+
|
|
117
|
+
parsed_schema = ActionSpec::Schema.schema_definition?(description) ? ActionSpec::Schema.from_definition(description) : nil
|
|
118
|
+
return [parsed_schema, nil, nil] if parsed_schema
|
|
119
|
+
|
|
120
|
+
[nil, description, options.presence]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def resolved_description(description, desc)
|
|
124
|
+
return description if description.is_a?(String)
|
|
125
|
+
return desc if desc.present?
|
|
126
|
+
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
90
130
|
def add_param(location_name, name, type, required:, **options)
|
|
91
|
-
|
|
92
|
-
endpoint.request.add_param(
|
|
131
|
+
required ||= options.delete(:required) == true
|
|
132
|
+
endpoint.request.add_param(
|
|
133
|
+
location_name,
|
|
134
|
+
ActionSpec::Schema.build_field(name, options.merge(type:), required:, scopes: scopes.dup)
|
|
135
|
+
)
|
|
93
136
|
end
|
|
94
137
|
|
|
95
138
|
def add_many(location_name, params, required:)
|
|
96
139
|
params.each_pair do |name, definition|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
location_name,
|
|
102
|
-
ActionSpec::Schema::Field.new(
|
|
103
|
-
name:,
|
|
104
|
-
required:,
|
|
105
|
-
schema: ActionSpec::Schema.from_definition(definition),
|
|
106
|
-
scopes: scopes.dup
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
else
|
|
110
|
-
add_param(location_name, name, String, required:, **definition)
|
|
111
|
-
end
|
|
112
|
-
elsif definition.is_a?(Hash)
|
|
113
|
-
add_param(location_name, name, definition[:type] || definition["type"] || String, required:, **definition.symbolize_keys.except(:type))
|
|
114
|
-
else
|
|
115
|
-
add_param(location_name, name, definition, required:)
|
|
116
|
-
end
|
|
140
|
+
endpoint.request.add_param(
|
|
141
|
+
location_name,
|
|
142
|
+
ActionSpec::Schema.build_field(name, definition, required:, scopes: scopes.dup)
|
|
143
|
+
)
|
|
117
144
|
end
|
|
118
145
|
end
|
|
119
146
|
|
|
120
|
-
def add_body(media_type, definition)
|
|
147
|
+
def add_body(media_type, definition, required:)
|
|
148
|
+
endpoint.request.require_body! if required
|
|
121
149
|
ActionSpec::Schema.build_fields(definition, scopes: scopes.dup).each_value do |field|
|
|
122
150
|
endpoint.request.add_body(media_type, field)
|
|
123
151
|
end
|
|
@@ -23,7 +23,8 @@ module ActionSpec
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def add_response(code, response)
|
|
26
|
-
@responses[code.to_s]
|
|
26
|
+
existing = @responses[code.to_s]
|
|
27
|
+
@responses[code.to_s] = existing ? existing.merge(response) : response
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def copy
|
|
@@ -46,6 +47,7 @@ module ActionSpec
|
|
|
46
47
|
@cookie = Location.new(:cookie)
|
|
47
48
|
@body = Location.new(:body)
|
|
48
49
|
@body_media_types = {}
|
|
50
|
+
@body_required = false
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def location(name)
|
|
@@ -61,6 +63,14 @@ module ActionSpec
|
|
|
61
63
|
(@body_media_types[media_type.to_sym] ||= Location.new(media_type.to_sym)).add(field.copy)
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def require_body!
|
|
67
|
+
@body_required = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def body_required?
|
|
71
|
+
@body_required
|
|
72
|
+
end
|
|
73
|
+
|
|
64
74
|
def replace_with(other)
|
|
65
75
|
@header = other.header
|
|
66
76
|
@path = other.path
|
|
@@ -68,6 +78,7 @@ module ActionSpec
|
|
|
68
78
|
@cookie = other.cookie
|
|
69
79
|
@body = other.body
|
|
70
80
|
@body_media_types = other.body_media_types
|
|
81
|
+
@body_required = other.body_required?
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
def copy
|
|
@@ -81,6 +92,7 @@ module ActionSpec
|
|
|
81
92
|
:@body_media_types,
|
|
82
93
|
body_media_types.transform_values(&:copy)
|
|
83
94
|
)
|
|
95
|
+
request.instance_variable_set(:@body_required, body_required?)
|
|
84
96
|
end
|
|
85
97
|
end
|
|
86
98
|
end
|
|
@@ -123,18 +135,87 @@ module ActionSpec
|
|
|
123
135
|
end
|
|
124
136
|
|
|
125
137
|
class Response
|
|
126
|
-
attr_reader :code, :description, :
|
|
138
|
+
attr_reader :code, :description, :media_types, :options
|
|
127
139
|
|
|
128
|
-
def initialize(code:, description:, media_type:, options:)
|
|
140
|
+
def initialize(code:, description:, media_type:, schema: nil, example: nil, examples: nil, options:)
|
|
129
141
|
@code = code.to_s
|
|
130
142
|
@description = description
|
|
131
|
-
@
|
|
132
|
-
@options = options
|
|
143
|
+
@media_types = ActiveSupport::OrderedHash.new
|
|
144
|
+
@options = options.deep_dup
|
|
145
|
+
merge_media_type!(media_type || :json, schema:, example:, examples:, initialize: true)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def merge(other)
|
|
149
|
+
copy.tap do |merged|
|
|
150
|
+
merged.instance_variable_set(:@description, other.description.presence || description)
|
|
151
|
+
merged.instance_variable_set(:@options, options.deep_dup.merge(other.options.deep_dup))
|
|
152
|
+
other.media_types.each do |media_type, content|
|
|
153
|
+
merged.send(:merge_media_type!, media_type, **merged.send(:copy_content, content))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
133
156
|
end
|
|
134
157
|
|
|
135
158
|
def copy
|
|
136
|
-
self.class.new(
|
|
159
|
+
self.class.new(
|
|
160
|
+
code:,
|
|
161
|
+
description:,
|
|
162
|
+
media_type: nil,
|
|
163
|
+
schema: nil,
|
|
164
|
+
example: nil,
|
|
165
|
+
examples: nil,
|
|
166
|
+
options: options.deep_dup
|
|
167
|
+
).tap do |response|
|
|
168
|
+
response.instance_variable_set(
|
|
169
|
+
:@media_types,
|
|
170
|
+
media_types.each_with_object(ActiveSupport::OrderedHash.new) do |(media_type, content), hash|
|
|
171
|
+
hash[media_type] = copy_content(content)
|
|
172
|
+
end
|
|
173
|
+
)
|
|
174
|
+
end
|
|
137
175
|
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def merge_media_type!(media_type, schema:, example:, examples:, initialize: false)
|
|
180
|
+
content = (@media_types[media_type.to_sym] ||= {
|
|
181
|
+
schema: nil,
|
|
182
|
+
example: nil,
|
|
183
|
+
examples: ActiveSupport::OrderedHash.new
|
|
184
|
+
})
|
|
185
|
+
content[:schema] = schema || content[:schema]
|
|
186
|
+
|
|
187
|
+
if examples.present?
|
|
188
|
+
convert_example_into_default_example!(content)
|
|
189
|
+
content[:examples].merge!(normalize_examples(examples))
|
|
190
|
+
elsif !example.nil?
|
|
191
|
+
if content[:examples].present? && !initialize
|
|
192
|
+
content[:examples]["default"] ||= example
|
|
193
|
+
else
|
|
194
|
+
content[:example] = example
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def convert_example_into_default_example!(content)
|
|
200
|
+
return if content[:example].nil?
|
|
201
|
+
|
|
202
|
+
content[:examples]["default"] ||= content[:example]
|
|
203
|
+
content[:example] = nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def normalize_examples(examples)
|
|
207
|
+
examples.each_with_object(ActiveSupport::OrderedHash.new) do |(name, value), hash|
|
|
208
|
+
hash[name.to_s] = value
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def copy_content(content)
|
|
213
|
+
{
|
|
214
|
+
schema: content[:schema]&.copy,
|
|
215
|
+
example: content[:example].deep_dup,
|
|
216
|
+
examples: content[:examples].deep_dup
|
|
217
|
+
}
|
|
218
|
+
end
|
|
138
219
|
end
|
|
139
220
|
end
|
|
140
221
|
end
|