reqcord 0.2.0 → 0.3.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/CHANGELOG.md +29 -1
- data/README.md +115 -746
- data/docs/architecture.md +134 -0
- data/docs/capture.md +169 -0
- data/docs/configuration.md +7 -2
- data/docs/exporters.md +182 -0
- data/docs/getting-started.md +186 -0
- data/docs/route-coverage.md +91 -0
- data/docs/troubleshooting.md +113 -0
- data/docs/web.md +66 -0
- data/lib/reqcord/capture/integration_patch.rb +9 -2
- data/lib/reqcord/check.rb +73 -0
- data/lib/reqcord/configuration.rb +9 -0
- data/lib/reqcord/dataset.rb +0 -1
- data/lib/reqcord/exporters/markdown.rb +5 -2
- data/lib/reqcord/exporters/openapi.rb +14 -2
- data/lib/reqcord/exporters/postman.rb +11 -1
- data/lib/reqcord/file_value.rb +62 -0
- data/lib/reqcord/generator.rb +17 -3
- data/lib/reqcord/renderers/curl.rb +22 -1
- data/lib/reqcord/renderers/payload.rb +25 -7
- data/lib/reqcord/route_collector.rb +10 -1
- data/lib/reqcord/schema.rb +11 -1
- data/lib/reqcord/version.rb +1 -1
- data/lib/reqcord.rb +2 -0
- data/lib/tasks/reqcord.rake +23 -0
- data/reqcord.gemspec +2 -1
- metadata +12 -2
data/README.md
CHANGED
|
@@ -1,97 +1,108 @@
|
|
|
1
1
|
# Reqcord
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ahmetsaridogan/reqcord/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/reqcord)
|
|
5
|
+
|
|
6
|
+
**Turn your Rails integration tests into API documentation.**
|
|
7
|
+
|
|
8
|
+
Reqcord runs your test suite, watches the HTTP requests and responses the
|
|
9
|
+
tests make, and writes the documentation from what it saw — Markdown,
|
|
10
|
+
runnable cURL, a Postman collection and an OpenAPI document. No DSL, no
|
|
11
|
+
annotations, no second copy of every request: the tests are the source of
|
|
12
|
+
truth.
|
|
13
|
+
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart LR
|
|
16
|
+
subgraph tests["Your integration tests"]
|
|
17
|
+
direction TB
|
|
18
|
+
T1["creates customer<br/>POST /api/v2/customers → 201"]
|
|
19
|
+
T2["rejects unknown status<br/>POST /api/v2/customers → 422"]
|
|
20
|
+
T3["requires authentication<br/>GET /api/v2/customers → 401"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
R["Rails route table"]
|
|
24
|
+
|
|
25
|
+
subgraph reqcord["Reqcord"]
|
|
26
|
+
direction TB
|
|
27
|
+
C["capture · sanitize · infer"]
|
|
28
|
+
D[("dataset.json")]
|
|
29
|
+
C --> D
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph out["Generated"]
|
|
33
|
+
direction TB
|
|
34
|
+
MD["Markdown pages"]
|
|
35
|
+
CU["cURL scripts"]
|
|
36
|
+
PM["Postman collection<br/>(Hoppscotch)"]
|
|
37
|
+
OA["OpenAPI 3.1"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
SC["Scalar<br/>mount Reqcord::Web"]
|
|
41
|
+
|
|
42
|
+
tests --> C
|
|
43
|
+
R --> C
|
|
44
|
+
D --> MD
|
|
45
|
+
D --> CU
|
|
46
|
+
D --> PM
|
|
47
|
+
D --> OA
|
|
48
|
+
OA --> SC
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
4
52
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
53
|
+
```ruby
|
|
54
|
+
# Gemfile
|
|
55
|
+
group :development, :test do
|
|
56
|
+
gem "reqcord"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
8
59
|
|
|
9
|
-
|
|
60
|
+
```bash
|
|
61
|
+
bundle install
|
|
62
|
+
bin/rails reqcord:init # writes reqcord.yml — point test.paths at your API tests
|
|
63
|
+
bin/rails reqcord:generate # runs them with capture on, writes docs/api/
|
|
64
|
+
```
|
|
10
65
|
|
|
11
66
|
```text
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
↓
|
|
16
|
-
Request Capture
|
|
17
|
-
↓
|
|
18
|
-
Response Capture
|
|
19
|
-
↓
|
|
20
|
-
Sanitization
|
|
21
|
-
↓
|
|
22
|
-
Canonical Dataset
|
|
23
|
-
↓
|
|
24
|
-
Markdown · cURL · Postman · OpenAPI (Scalar)
|
|
67
|
+
[reqcord] captured 87 request(s), 85 matched a documented route
|
|
68
|
+
[reqcord] captured a successful 2xx request for 15 of 16 endpoint(s)
|
|
69
|
+
[reqcord] routes: 18 = 15 documented + 1 uncovered + 2 skipped
|
|
25
70
|
```
|
|
26
71
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
A request changes.
|
|
32
|
-
|
|
33
|
-
A header is added.
|
|
34
|
-
|
|
35
|
-
A validation rule changes.
|
|
72
|
+
Every route ends in exactly one bucket, so nothing goes missing quietly. In
|
|
73
|
+
CI, `bin/rails reqcord:check` fails when the committed docs are behind the
|
|
74
|
+
tests.
|
|
36
75
|
|
|
37
|
-
|
|
76
|
+
Optionally, browse it inside the app with Scalar:
|
|
38
77
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Reqcord
|
|
42
|
-
|
|
43
|
-
> If your tests already know how to call your API, they already contain most of the information required to document it.
|
|
44
|
-
|
|
45
|
-
Reqcord captures that information and turns it into static API documentation.
|
|
46
|
-
|
|
47
|
-
No separate documentation DSL.
|
|
48
|
-
|
|
49
|
-
No duplicate request definitions.
|
|
50
|
-
|
|
51
|
-
No manually maintained cURL examples.
|
|
52
|
-
|
|
53
|
-
Your tests remain normal Rails tests.
|
|
78
|
+
```ruby
|
|
79
|
+
# config/routes.rb
|
|
80
|
+
mount Reqcord::Web => "/api-docs" if Rails.env.development?
|
|
81
|
+
```
|
|
54
82
|
|
|
55
|
-
##
|
|
83
|
+
## What you get
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
From an ordinary test —
|
|
58
86
|
|
|
59
87
|
```ruby
|
|
60
88
|
test "creates customer" do
|
|
61
89
|
post "/api/v2/customers",
|
|
62
|
-
params: {
|
|
63
|
-
|
|
64
|
-
name: "John Doe",
|
|
65
|
-
email: "john@example.com"
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
headers: {
|
|
69
|
-
"Authorization" => "Bearer test-token",
|
|
70
|
-
"X-Account-Id" => "42"
|
|
71
|
-
},
|
|
90
|
+
params: { customer: { name: "John Doe", email: "john@example.com", status: "active" } },
|
|
91
|
+
headers: { "Authorization" => "Bearer test-token" },
|
|
72
92
|
as: :json
|
|
73
93
|
|
|
74
94
|
assert_response :created
|
|
75
95
|
end
|
|
76
96
|
```
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
It can generate documentation such as:
|
|
98
|
+
— a page like this, plus a `create.sh`, a Postman request and an OpenAPI
|
|
99
|
+
operation built from the same captured request:
|
|
81
100
|
|
|
82
101
|
````markdown
|
|
83
102
|
# Create Customer
|
|
84
103
|
|
|
85
104
|
`POST /api/v2/customers`
|
|
86
105
|
|
|
87
|
-
## Headers
|
|
88
|
-
|
|
89
|
-
| Header | Value |
|
|
90
|
-
| --- | --- |
|
|
91
|
-
| Authorization | `Bearer {{token}}` |
|
|
92
|
-
| X-Account-Id | `{{account_id}}` |
|
|
93
|
-
| Content-Type | `application/json` |
|
|
94
|
-
|
|
95
106
|
## Body Parameters
|
|
96
107
|
|
|
97
108
|
| Field | Type | Required | Values |
|
|
@@ -100,121 +111,57 @@ It can generate documentation such as:
|
|
|
100
111
|
| `customer.email` | string | yes | `"john@example.com"` |
|
|
101
112
|
| `customer.status` | string | yes | `"active"` \| `"passive"` |
|
|
102
113
|
|
|
103
|
-
## Example Request
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"customer": {
|
|
108
|
-
"name": "John Doe",
|
|
109
|
-
"email": "john@example.com",
|
|
110
|
-
"status": "active"
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
114
|
## cURL
|
|
116
115
|
|
|
117
116
|
```bash
|
|
118
117
|
curl --request POST \
|
|
119
118
|
--url "http://localhost:3000/api/v2/customers" \
|
|
120
119
|
--header "Authorization: Bearer {{token}}" \
|
|
121
|
-
--header "X-Account-Id: {{account_id}}" \
|
|
122
120
|
--header "Content-Type: application/json" \
|
|
123
|
-
--data '{
|
|
124
|
-
"customer": {
|
|
125
|
-
"name": "John Doe",
|
|
126
|
-
"email": "john@example.com",
|
|
127
|
-
"status": "active"
|
|
128
|
-
}
|
|
129
|
-
}'
|
|
121
|
+
--data '{"customer":{"name":"John Doe","email":"john@example.com","status":"active"}}'
|
|
130
122
|
```
|
|
131
123
|
|
|
132
124
|
## Responses
|
|
133
125
|
|
|
134
126
|
### 201 Created
|
|
135
|
-
|
|
136
|
-
#### Fields
|
|
137
|
-
|
|
138
|
-
| Field | Type | Required | Values |
|
|
139
|
-
| --- | --- | --- | --- |
|
|
140
|
-
| `id` | integer | yes | `42` |
|
|
141
|
-
| `name` | string | yes | `"John Doe"` |
|
|
142
|
-
|
|
143
|
-
```json
|
|
144
|
-
{
|
|
145
|
-
"id": 42,
|
|
146
|
-
"name": "John Doe"
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
127
|
### 401 Unauthorized
|
|
151
|
-
|
|
152
|
-
```json
|
|
153
|
-
{
|
|
154
|
-
"error": "Unauthorized"
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
128
|
### 422 Unprocessable Content
|
|
159
|
-
|
|
160
|
-
```json
|
|
161
|
-
{
|
|
162
|
-
"errors": {
|
|
163
|
-
"email": [
|
|
164
|
-
"has already been taken"
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
129
|
````
|
|
170
130
|
|
|
171
|
-
The parameter
|
|
172
|
-
**accepted**: two
|
|
173
|
-
`"inactive"` and got a `422`, so the
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
with this request and its three saved responses, and an OpenAPI 3.1 document
|
|
177
|
-
you can browse with Scalar by mounting `Reqcord::Web` (see
|
|
178
|
-
[Serve the docs in your app](#serve-the-docs-in-your-app)).
|
|
179
|
-
|
|
180
|
-
## Examples
|
|
131
|
+
The parameter table is inferred from the requests the application
|
|
132
|
+
**accepted**: two tests sent `"active"` and `"passive"`, a third sent
|
|
133
|
+
`"inactive"` and got a `422`, so the page lists the two values that work and
|
|
134
|
+
keeps the rejection as a response example. Credentials never reach a file —
|
|
135
|
+
`Bearer test-token` became `Bearer {{token}}` before anything was stored.
|
|
181
136
|
|
|
182
|
-
|
|
137
|
+
## Documentation
|
|
183
138
|
|
|
184
|
-
|
|
|
185
|
-
| --- | --- |
|
|
186
|
-
| [
|
|
187
|
-
| [
|
|
188
|
-
| [
|
|
189
|
-
| [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
## Core Idea
|
|
139
|
+
| | |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| [Getting started](docs/getting-started.md) | install, configure, generate, read the output |
|
|
142
|
+
| [Configuration](docs/configuration.md) | every key in `reqcord.yml`, defaults and environment overrides |
|
|
143
|
+
| [Exporters](docs/exporters.md) | Markdown, cURL, Postman / Hoppscotch, OpenAPI — what each contains |
|
|
144
|
+
| [Reqcord::Web](docs/web.md) | serve the docs from the app with Scalar |
|
|
145
|
+
| [Route coverage](docs/route-coverage.md) | how the route table becomes endpoints; `resource`, `match via:`, engines, filters |
|
|
146
|
+
| [Capture and inference](docs/capture.md) | what is captured, sanitization, how parameter tables and response fields are derived, the dataset |
|
|
147
|
+
| [Troubleshooting](docs/troubleshooting.md) | when the output looks thin |
|
|
148
|
+
| [Architecture](docs/architecture.md) | pipeline, modules, design principles, working on Reqcord |
|
|
149
|
+
| [Changelog](CHANGELOG.md) | |
|
|
196
150
|
|
|
197
|
-
|
|
151
|
+
## Examples
|
|
198
152
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
│
|
|
202
|
-
▼
|
|
203
|
-
Route Collector
|
|
204
|
-
│
|
|
205
|
-
Minitest ──────► Test Adapter
|
|
206
|
-
│
|
|
207
|
-
▼
|
|
208
|
-
Reqcord Dataset
|
|
209
|
-
│
|
|
210
|
-
┌────────────┼────────────┬────────────┐
|
|
211
|
-
▼ ▼ ▼ ▼
|
|
212
|
-
Markdown cURL Postman OpenAPI ──► Scalar
|
|
213
|
-
```
|
|
153
|
+
Four runnable applications under [`examples/`](examples), each with the
|
|
154
|
+
documentation it generates committed next to it:
|
|
214
155
|
|
|
215
|
-
|
|
156
|
+
| Example | Tests | Shows |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| [`test-app`](examples/test-app) | Minitest | three small resources: auth, closed value sets, PATCH/PUT folding, a member action |
|
|
159
|
+
| [`spec-app`](examples/spec-app) | RSpec | the same API from request specs |
|
|
160
|
+
| [`complex-test-app`](examples/complex-test-app) | Minitest | a store API: products, cart, orders, nested notes, array bodies, `filter[category]`, a form login, `X-Api-Key` admin namespace, two API versions, 400/403/404/409 |
|
|
161
|
+
| [`complex-spec-app`](examples/complex-spec-app) | RSpec | the store API from request specs |
|
|
216
162
|
|
|
217
|
-
|
|
163
|
+
[`examples/reqcord.yml`](examples/reqcord.yml) is an annotated configuration
|
|
164
|
+
file.
|
|
218
165
|
|
|
219
166
|
## Supported versions
|
|
220
167
|
|
|
@@ -222,597 +169,19 @@ This allows Reqcord to support additional test frameworks and documentation form
|
|
|
222
169
|
| --- | --- |
|
|
223
170
|
| Ruby | 3.2, 3.3, 3.4 |
|
|
224
171
|
| Rails | 7.1, 7.2, 8.0, 8.1 |
|
|
225
|
-
|
|
|
226
|
-
|
|
227
|
-
Every Ruby × Rails pair that Rails itself supports runs in CI
|
|
228
|
-
(`gemfiles/rails_*.gemfile`).
|
|
229
|
-
|
|
230
|
-
## Installation
|
|
231
|
-
|
|
232
|
-
Add Reqcord to the development and test groups:
|
|
233
|
-
|
|
234
|
-
```ruby
|
|
235
|
-
group :development, :test do
|
|
236
|
-
gem "reqcord"
|
|
237
|
-
end
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Then run:
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
bundle install
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Initialize Reqcord:
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
bin/rails reqcord:init
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
This creates:
|
|
253
|
-
|
|
254
|
-
```text
|
|
255
|
-
reqcord.yml
|
|
256
|
-
docs/
|
|
257
|
-
└── api/
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
## Configuration
|
|
261
|
-
|
|
262
|
-
Reqcord reads its configuration from `reqcord.yml` in the project root. Every
|
|
263
|
-
key, default and environment override is described in
|
|
264
|
-
[docs/configuration.md](docs/configuration.md); the short version:
|
|
265
|
-
|
|
266
|
-
```yaml
|
|
267
|
-
version: 1
|
|
268
|
-
|
|
269
|
-
test:
|
|
270
|
-
framework: minitest # or: rspec
|
|
271
|
-
|
|
272
|
-
routes:
|
|
273
|
-
prefix: /api
|
|
274
|
-
|
|
275
|
-
output:
|
|
276
|
-
directory: docs/api
|
|
277
|
-
|
|
278
|
-
exporters:
|
|
279
|
-
- curl
|
|
280
|
-
- markdown
|
|
281
|
-
- postman
|
|
282
|
-
- openapi
|
|
283
|
-
|
|
284
|
-
variables:
|
|
285
|
-
base_url: http://localhost:3000
|
|
286
|
-
|
|
287
|
-
sanitize:
|
|
288
|
-
headers:
|
|
289
|
-
Authorization: "Bearer {{token}}"
|
|
290
|
-
X-Account-Id: "{{account_id}}"
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Configuration precedence:
|
|
294
|
-
|
|
295
|
-
```text
|
|
296
|
-
CLI / Environment
|
|
297
|
-
↓
|
|
298
|
-
reqcord.yml
|
|
299
|
-
↓
|
|
300
|
-
Reqcord defaults
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Generating Documentation
|
|
172
|
+
| Tests | Minitest integration tests, RSpec request specs |
|
|
304
173
|
|
|
305
|
-
|
|
174
|
+
Every Ruby × Rails pair Rails itself supports runs in CI.
|
|
306
175
|
|
|
307
|
-
|
|
308
|
-
bin/rails reqcord:generate
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
Generate documentation for a specific resource:
|
|
312
|
-
|
|
313
|
-
```bash
|
|
314
|
-
bin/rails reqcord:generate RESOURCE=customers
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Multiple resources:
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
bin/rails reqcord:generate RESOURCE=customers,surveys
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Filter by API version:
|
|
324
|
-
|
|
325
|
-
```bash
|
|
326
|
-
bin/rails reqcord:generate VERSION=v2
|
|
327
|
-
```
|
|
176
|
+
## Principles
|
|
328
177
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
`reqcord:generate` runs the test suite itself, in a subprocess, with capture
|
|
336
|
-
enabled:
|
|
337
|
-
|
|
338
|
-
```text
|
|
339
|
-
bin/rails reqcord:generate
|
|
340
|
-
|
|
|
341
|
-
+-- collects the application's routes
|
|
342
|
-
|
|
|
343
|
-
+-- runs `test.command` with REQCORD_CAPTURE=1
|
|
344
|
-
| |
|
|
345
|
-
| +-- each request appends a JSON line to the capture file
|
|
346
|
-
|
|
|
347
|
-
+-- reads the capture file, sanitizes, writes the documentation
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Because capture is driven by `REQCORD_CAPTURE` and `REQCORD_CAPTURE_FILE`, an
|
|
351
|
-
ordinary `bin/rails test` patches nothing and writes nothing. The capture file
|
|
352
|
-
is append-only and locked per write, so parallel test workers can share it.
|
|
353
|
-
|
|
354
|
-
Point Reqcord at the tests that exercise the API — a directory is enough, it
|
|
355
|
-
picks the runner (`bin/rails test`, `rspec`, or a plain Ruby runner when the
|
|
356
|
-
project has no `bin/rails`):
|
|
357
|
-
|
|
358
|
-
```yaml
|
|
359
|
-
test:
|
|
360
|
-
framework: minitest
|
|
361
|
-
paths:
|
|
362
|
-
- test/integration
|
|
363
|
-
- test/api
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
Or spell the command out; it wins over `paths`, and globs are expanded:
|
|
367
|
-
|
|
368
|
-
```yaml
|
|
369
|
-
test:
|
|
370
|
-
command: bin/rails test test/integration test/api/*_test.rb
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
The run ends with a reconciliation of the whole route table, so nothing can
|
|
374
|
-
go missing quietly:
|
|
375
|
-
|
|
376
|
-
```text
|
|
377
|
-
[reqcord] captured 87 request(s), 85 matched a documented route
|
|
378
|
-
[reqcord] captured a successful 2xx request for 15 of 16 endpoint(s)
|
|
379
|
-
[reqcord] routes: 18 = 15 documented + 1 uncovered + 2 skipped
|
|
380
|
-
[reqcord] skipped 2 route(s) that cannot be documented: 1 redirect, 1 mount
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
Every route is in exactly one bucket: *documented* (a test got a `2xx`),
|
|
384
|
-
*uncovered* (listed in the index, no page), or *skipped* with its reason.
|
|
385
|
-
|
|
386
|
-
## Generated Files
|
|
387
|
-
|
|
388
|
-
Directories follow the controller path, so `admin/customers` and
|
|
389
|
-
`api/v2/customers` never collide:
|
|
390
|
-
|
|
391
|
-
```text
|
|
392
|
-
docs/api/
|
|
393
|
-
├── dataset.json
|
|
394
|
-
├── README.md
|
|
395
|
-
├── api/v2/customers/
|
|
396
|
-
│ ├── index.md
|
|
397
|
-
│ ├── create.md
|
|
398
|
-
│ ├── show.md
|
|
399
|
-
│ └── update.md
|
|
400
|
-
├── api/v2/surveys/
|
|
401
|
-
│ ├── index.md
|
|
402
|
-
│ └── list.md
|
|
403
|
-
├── curl/
|
|
404
|
-
│ └── api/v2/customers/
|
|
405
|
-
│ ├── create.sh
|
|
406
|
-
│ └── show.sh
|
|
407
|
-
├── postman/
|
|
408
|
-
│ └── collection.json
|
|
409
|
-
└── openapi/
|
|
410
|
-
└── openapi.json
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
`dataset.json` contains Reqcord's normalized representation of the captured
|
|
414
|
-
API; every exporter reads that and nothing else.
|
|
415
|
-
|
|
416
|
-
## Route coverage
|
|
417
|
-
|
|
418
|
-
The documented surface is the route table, not only `resources`. These all
|
|
419
|
-
become endpoints:
|
|
420
|
-
|
|
421
|
-
| Route | Documented as |
|
|
422
|
-
| --- | --- |
|
|
423
|
-
| `resources :customers` | one endpoint per action |
|
|
424
|
-
| `resource :cart` | `GET /cart`, `PATCH /cart` (also `PUT`) |
|
|
425
|
-
| `match "/echo", via: [:get, :post]` | `GET /echo` and `POST /echo` |
|
|
426
|
-
| `match "/anything", via: :all` | one endpoint per verb the tests used |
|
|
427
|
-
| `root to: "home#index"` | `GET /`, titled "Home" |
|
|
428
|
-
| `get "/items(/:id)"` | one endpoint, `:id` optional |
|
|
429
|
-
| `get "/files/*path"` | `path` as a path parameter |
|
|
430
|
-
| `mount Billing => "/billing"` | the engine's own routes, under `/billing` |
|
|
431
|
-
| `namespace :admin { resources :customers }` | `admin/customers/`, apart from `api/v2/customers/` |
|
|
432
|
-
|
|
433
|
-
`redirect(...)` routes and plain Rack mounts cannot be documented from a test;
|
|
434
|
-
they are counted as *skipped* in the report rather than dropped.
|
|
435
|
-
|
|
436
|
-
## Postman and Hoppscotch
|
|
437
|
-
|
|
438
|
-
`postman/collection.json` is a Postman Collection v2.1:
|
|
439
|
-
|
|
440
|
-
* one folder per controller namespace (`Api › V2 › Customers`),
|
|
441
|
-
* one request per documented endpoint, built from the successful captured
|
|
442
|
-
example — JSON bodies as `raw`, form bodies as `urlencoded`,
|
|
443
|
-
* every captured status saved as a response example on that request,
|
|
444
|
-
* collection variables for `base_url` and every placeholder the sanitizer
|
|
445
|
-
wrote (`{{token}}`, `{{api_key}}` …) — Postman's variable syntax is the
|
|
446
|
-
same, so the collection is usable as soon as the variables are filled in,
|
|
447
|
-
* `Authorization: Bearer {{token}}` lifted to collection-level bearer auth;
|
|
448
|
-
requests that were made without credentials are marked `noauth`, so they
|
|
449
|
-
replay exactly as their tests did.
|
|
450
|
-
|
|
451
|
-
Hoppscotch imports Postman v2.1 collections directly: *Import → Postman* and
|
|
452
|
-
point it at the same file.
|
|
453
|
-
|
|
454
|
-
## OpenAPI
|
|
455
|
-
|
|
456
|
-
`openapi/openapi.json` is an OpenAPI 3.1 document built from the same dataset:
|
|
457
|
-
|
|
458
|
-
* one path item per documented route, in OpenAPI notation — `/customers/:id`
|
|
459
|
-
becomes `/customers/{id}`, and `/items(/:id)` becomes both `/items` and
|
|
460
|
-
`/items/{id}`,
|
|
461
|
-
* path and query parameters from the inferred schemas, closed value sets as
|
|
462
|
-
`enum`,
|
|
463
|
-
* a `requestBody` (`application/json` or `application/x-www-form-urlencoded`,
|
|
464
|
-
whichever the test sent) whose JSON Schema is rebuilt from the field paths,
|
|
465
|
-
nested objects and arrays included, `required` from what every accepted
|
|
466
|
-
request carried,
|
|
467
|
-
* one response per captured status with its schema and example,
|
|
468
|
-
* `bearerAuth` / `apiKeyAuth` security schemes derived from the sanitized
|
|
469
|
-
`Authorization` and `X-Api-Key` headers, applied per operation — so public
|
|
470
|
-
endpoints stay public.
|
|
471
|
-
|
|
472
|
-
Anything that reads OpenAPI (Scalar, Swagger UI, Redoc, code generators) can
|
|
473
|
-
consume the file as is.
|
|
474
|
-
|
|
475
|
-
## Serve the docs in your app
|
|
476
|
-
|
|
477
|
-
`Reqcord::Web` is a Rack application that serves the generated output from
|
|
478
|
-
inside the Rails app, the way `Sidekiq::Web` does:
|
|
479
|
-
|
|
480
|
-
```ruby
|
|
481
|
-
# config/routes.rb
|
|
482
|
-
mount Reqcord::Web => "/api-docs" if Rails.env.development?
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
* `/api-docs` renders `openapi/openapi.json` with
|
|
486
|
-
[Scalar](https://scalar.com) — a searchable reference with a *Try it*
|
|
487
|
-
client, loaded from the Scalar CDN,
|
|
488
|
-
* `/api-docs/openapi/openapi.json`, `/api-docs/dataset.json`,
|
|
489
|
-
`/api-docs/postman/collection.json`, `/api-docs/api/v2/customers/create.md`,
|
|
490
|
-
`/api-docs/curl/api/v2/customers/create.sh` … serve the generated files,
|
|
491
|
-
* nothing outside `output.directory` is ever served.
|
|
492
|
-
|
|
493
|
-
`Reqcord::Web` only reads; run `bin/rails reqcord:generate` first (before the
|
|
494
|
-
first run the page tells you so). Because the files are static, mounting it
|
|
495
|
-
in production is a deployment decision, not a Reqcord one — guard it as you
|
|
496
|
-
would any internal page.
|
|
497
|
-
|
|
498
|
-
## Request Capture
|
|
499
|
-
|
|
500
|
-
Reqcord captures HTTP information from Rails integration tests.
|
|
501
|
-
|
|
502
|
-
The initial version supports:
|
|
503
|
-
|
|
504
|
-
* HTTP method
|
|
505
|
-
* Request path
|
|
506
|
-
* Path parameters
|
|
507
|
-
* Query parameters
|
|
508
|
-
* Request headers
|
|
509
|
-
* JSON request bodies
|
|
510
|
-
* Content type
|
|
511
|
-
* Response status
|
|
512
|
-
* Response headers
|
|
513
|
-
* JSON response bodies
|
|
514
|
-
* Test name and source
|
|
515
|
-
* Multiple request/response examples per endpoint
|
|
516
|
-
|
|
517
|
-
Supported HTTP methods:
|
|
518
|
-
|
|
519
|
-
```text
|
|
520
|
-
GET
|
|
521
|
-
POST
|
|
522
|
-
PUT
|
|
523
|
-
PATCH
|
|
524
|
-
DELETE
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
## Multiple Responses
|
|
528
|
-
|
|
529
|
-
Reqcord does not assume that an endpoint has only one response.
|
|
530
|
-
|
|
531
|
-
For example:
|
|
532
|
-
|
|
533
|
-
```ruby
|
|
534
|
-
test "creates customer" do
|
|
535
|
-
# ...
|
|
536
|
-
assert_response :created
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
test "requires authentication" do
|
|
540
|
-
# ...
|
|
541
|
-
assert_response :unauthorized
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
test "rejects duplicate email" do
|
|
545
|
-
# ...
|
|
546
|
-
assert_response :unprocessable_entity
|
|
547
|
-
end
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
can produce:
|
|
551
|
-
|
|
552
|
-
```text
|
|
553
|
-
POST /api/v2/customers
|
|
554
|
-
|
|
555
|
-
Responses
|
|
556
|
-
├── 201 Created
|
|
557
|
-
├── 401 Unauthorized
|
|
558
|
-
└── 422 Unprocessable Entity
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
Every distinct body captured for a status is kept in `dataset.json`, and the
|
|
562
|
-
fields of a response are inferred from all of them:
|
|
563
|
-
|
|
564
|
-
```text
|
|
565
|
-
422 Unprocessable Content
|
|
566
|
-
├── Email already exists
|
|
567
|
-
├── Name is required
|
|
568
|
-
└── Invalid phone number
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
The Markdown page shows one example body per status plus the inferred field
|
|
572
|
-
table; Reqcord does not overwrite one `422` example with another.
|
|
573
|
-
|
|
574
|
-
## Sanitization
|
|
575
|
-
|
|
576
|
-
Captured tests may contain credentials or other sensitive values.
|
|
577
|
-
|
|
578
|
-
Reqcord must never blindly write those values into generated documentation.
|
|
579
|
-
|
|
580
|
-
Sensitive headers can be replaced with variables:
|
|
581
|
-
|
|
582
|
-
```yaml
|
|
583
|
-
sanitize:
|
|
584
|
-
headers:
|
|
585
|
-
Authorization: "Bearer {{token}}"
|
|
586
|
-
X-Api-Key: "{{api_key}}"
|
|
587
|
-
X-Account-Id: "{{account_id}}"
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
A captured request such as:
|
|
591
|
-
|
|
592
|
-
```text
|
|
593
|
-
Authorization: Bearer eyJhbGciOi...
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
becomes:
|
|
597
|
-
|
|
598
|
-
```text
|
|
599
|
-
Authorization: Bearer {{token}}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
Sensitive headers such as authorization credentials, cookies and API keys are treated specially by Reqcord.
|
|
603
|
-
|
|
604
|
-
Request and response bodies follow the same principle, matched by key at any
|
|
605
|
-
depth:
|
|
606
|
-
|
|
607
|
-
```yaml
|
|
608
|
-
sanitize:
|
|
609
|
-
body:
|
|
610
|
-
password: "{{password}}"
|
|
611
|
-
access_token: "{{token}}"
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
## Canonical Dataset
|
|
615
|
-
|
|
616
|
-
Reqcord does not directly convert Minitest tests into Markdown.
|
|
617
|
-
|
|
618
|
-
Instead:
|
|
619
|
-
|
|
620
|
-
```text
|
|
621
|
-
Minitest
|
|
622
|
-
↓
|
|
623
|
-
Test Adapter
|
|
624
|
-
↓
|
|
625
|
-
Canonical Dataset
|
|
626
|
-
↓
|
|
627
|
-
Exporter
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
A simplified endpoint representation looks like:
|
|
631
|
-
|
|
632
|
-
```json
|
|
633
|
-
{
|
|
634
|
-
"name": "Create Customer",
|
|
635
|
-
"method": "POST",
|
|
636
|
-
"path": "/api/v2/customers",
|
|
637
|
-
"controller": "api/v2/customers",
|
|
638
|
-
"action": "create",
|
|
639
|
-
"parameters": {
|
|
640
|
-
"path": [],
|
|
641
|
-
"query": [],
|
|
642
|
-
"body": [
|
|
643
|
-
{ "path": "customer.name", "type": "string", "required": true, "values": ["John Doe"] },
|
|
644
|
-
{ "path": "customer.status", "type": "string", "required": true, "values": ["active", "passive"] }
|
|
645
|
-
]
|
|
646
|
-
},
|
|
647
|
-
"responses": [
|
|
648
|
-
{
|
|
649
|
-
"status": 201,
|
|
650
|
-
"schema": [
|
|
651
|
-
{ "path": "id", "type": "integer", "required": true, "values": [42] }
|
|
652
|
-
],
|
|
653
|
-
"example": { "id": 42, "name": "John Doe" }
|
|
654
|
-
},
|
|
655
|
-
{ "status": 401, "schema": [ { "path": "error", "type": "string", "required": true, "values": ["Unauthorized"] } ], "example": { "error": "Unauthorized" } }
|
|
656
|
-
],
|
|
657
|
-
"request_examples": [ "… every captured request, sanitized" ],
|
|
658
|
-
"response_examples": [ "… every captured response, sanitized" ]
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
`parameters` and `responses[].schema` are inferred only from requests the
|
|
663
|
-
application accepted; `request_examples` keeps everything that was captured.
|
|
664
|
-
Routes no test reached are listed separately under `uncovered_routes`.
|
|
665
|
-
|
|
666
|
-
Every dataset contains a schema version so the internal format can evolve safely.
|
|
667
|
-
|
|
668
|
-
```json
|
|
669
|
-
{
|
|
670
|
-
"schema_version": 2
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
## Architecture
|
|
675
|
-
|
|
676
|
-
```text
|
|
677
|
-
Reqcord
|
|
678
|
-
├── Configuration
|
|
679
|
-
├── Dataset
|
|
680
|
-
│ ├── Resource (one controller path, nested directories/folders)
|
|
681
|
-
│ ├── Endpoint
|
|
682
|
-
│ ├── RequestExample
|
|
683
|
-
│ ├── ResponseExample
|
|
684
|
-
│ └── Schema (fields, types, required, closed value sets)
|
|
685
|
-
│
|
|
686
|
-
├── RouteCollector (every route kind, engines walked, skips counted)
|
|
687
|
-
│
|
|
688
|
-
├── Capture
|
|
689
|
-
│ ├── Collector (NDJSON, one line per exchange)
|
|
690
|
-
│ ├── TestContext
|
|
691
|
-
│ ├── MinitestContext / RSpecContext
|
|
692
|
-
│ └── IntegrationPatch
|
|
693
|
-
│
|
|
694
|
-
├── Generator (run tests → dataset → exporters → report)
|
|
695
|
-
│
|
|
696
|
-
├── Sanitizers
|
|
697
|
-
│ └── Sanitizer (headers and bodies)
|
|
698
|
-
│
|
|
699
|
-
├── Renderers
|
|
700
|
-
│ ├── Payload (JSON vs form, nested query flattening)
|
|
701
|
-
│ └── Curl
|
|
702
|
-
│
|
|
703
|
-
├── Exporters
|
|
704
|
-
│ ├── Markdown
|
|
705
|
-
│ ├── Curl (one .sh per endpoint)
|
|
706
|
-
│ ├── Postman (Collection v2.1, also for Hoppscotch)
|
|
707
|
-
│ └── Openapi (OpenAPI 3.1)
|
|
708
|
-
│
|
|
709
|
-
└── Web (Rack app: Scalar page + generated files)
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
Test adapters are responsible only for converting test execution into Reqcord's canonical model.
|
|
713
|
-
|
|
714
|
-
Exporters know nothing about Minitest or Rails test internals.
|
|
715
|
-
|
|
716
|
-
```text
|
|
717
|
-
Minitest ──┐
|
|
718
|
-
│
|
|
719
|
-
RSpec ─────┼──► Dataset ──► Markdown
|
|
720
|
-
│ ├─► cURL
|
|
721
|
-
Other ─────┘ ├─► Postman (→ Hoppscotch)
|
|
722
|
-
└─► OpenAPI (→ Scalar via Reqcord::Web)
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
## v0.1 Scope
|
|
726
|
-
|
|
727
|
-
The first Reqcord release focuses on proving the capture pipeline.
|
|
728
|
-
|
|
729
|
-
### Included
|
|
730
|
-
|
|
731
|
-
* Rails 8
|
|
732
|
-
* Minitest integration tests
|
|
733
|
-
* RSpec request specs
|
|
734
|
-
* Rails route discovery
|
|
735
|
-
* `reqcord.yml`
|
|
736
|
-
* Request capture
|
|
737
|
-
* Response capture
|
|
738
|
-
* Multiple response scenarios
|
|
739
|
-
* Sensitive data sanitization
|
|
740
|
-
* Canonical `dataset.json` with inferred request parameters and response fields
|
|
741
|
-
* Markdown documentation
|
|
742
|
-
* Generated cURL requests (in the Markdown and as runnable `.sh` files)
|
|
743
|
-
* Postman Collection v2.1 (imports into Hoppscotch as well)
|
|
744
|
-
* OpenAPI 3.1 document, browsable with Scalar through `mount Reqcord::Web`
|
|
745
|
-
* The whole route table: custom actions, `match via:`, `via: :all`,
|
|
746
|
-
singular resources, optional segments and globs, mounted engines
|
|
747
|
-
* Resource filtering
|
|
748
|
-
* API version filtering
|
|
749
|
-
|
|
750
|
-
### Not included yet
|
|
751
|
-
|
|
752
|
-
* Multipart requests
|
|
753
|
-
* CI documentation drift detection
|
|
754
|
-
|
|
755
|
-
These features belong to later releases rather than expanding the initial scope.
|
|
756
|
-
|
|
757
|
-
## Roadmap
|
|
758
|
-
|
|
759
|
-
### v0.3
|
|
760
|
-
|
|
761
|
-
Rack::Test capture, so frameworks other than Rails (Sinatra, Roda, Hanami) can
|
|
762
|
-
be documented from the same dataset.
|
|
763
|
-
|
|
764
|
-
### Future
|
|
765
|
-
|
|
766
|
-
Potential exporters and integrations include:
|
|
767
|
-
|
|
768
|
-
* Bruno
|
|
769
|
-
* Insomnia
|
|
770
|
-
* `llms.txt`
|
|
771
|
-
* Static HTML documentation
|
|
772
|
-
* JSON Schema
|
|
773
|
-
* CI documentation drift detection
|
|
774
|
-
|
|
775
|
-
## Design Principles
|
|
776
|
-
|
|
777
|
-
**Tests are the source of truth.**
|
|
778
|
-
|
|
779
|
-
Reqcord should observe existing tests instead of forcing developers to rewrite them using a documentation-specific DSL.
|
|
780
|
-
|
|
781
|
-
**Capture once, export anywhere.**
|
|
782
|
-
|
|
783
|
-
Test execution produces a framework-independent dataset. Exporters operate exclusively on that dataset.
|
|
784
|
-
|
|
785
|
-
**Generated documentation must be safe.**
|
|
786
|
-
|
|
787
|
-
Credentials and sensitive data must not leak into generated files.
|
|
788
|
-
|
|
789
|
-
**Generated documentation must be useful without a server.**
|
|
790
|
-
|
|
791
|
-
Markdown and cURL output should remain readable directly from GitHub or a local checkout.
|
|
792
|
-
|
|
793
|
-
**Adapters stay isolated.**
|
|
794
|
-
|
|
795
|
-
Minitest, RSpec, Markdown, OpenAPI and other integrations should not depend directly on each other.
|
|
796
|
-
|
|
797
|
-
## Status
|
|
798
|
-
|
|
799
|
-
Reqcord is currently in early development.
|
|
800
|
-
|
|
801
|
-
The initial goal is intentionally narrow:
|
|
802
|
-
|
|
803
|
-
> Capture real Rails API requests and responses from the test suite and generate accurate, sanitized Markdown documentation, executable cURL examples, a Postman collection and an OpenAPI document — without the developer writing any of them by hand.
|
|
804
|
-
|
|
805
|
-
Once that pipeline is reliable, additional adapters and exporters can be built on top of the same dataset.
|
|
178
|
+
* **Tests are the source of truth.** Reqcord never reads models, serializers
|
|
179
|
+
or contracts; what the application accepted and answered is the spec.
|
|
180
|
+
* **Capture once, export anywhere.** One dataset, any number of formats.
|
|
181
|
+
* **Nothing is lost silently.** `routes = documented + uncovered + skipped`,
|
|
182
|
+
reconciled on every run.
|
|
183
|
+
* **Generated docs are safe.** Sanitization runs before anything is stored.
|
|
806
184
|
|
|
807
185
|
## License
|
|
808
186
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
## cURL source of truth
|
|
812
|
-
|
|
813
|
-
Reqcord does not invent request payloads. For Rails integration tests, the
|
|
814
|
-
arguments passed to `get`, `post`, `put`, `patch`, and `delete` are captured at
|
|
815
|
-
runtime. Generated cURL commands use a successful `2xx` test case whenever one
|
|
816
|
-
exists, including its concrete URL, request headers, query parameters, and
|
|
817
|
-
payload. Error-case payloads remain available as examples but do not replace
|
|
818
|
-
the canonical successful request.
|
|
187
|
+
MIT.
|