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.
data/README.md CHANGED
@@ -1,97 +1,108 @@
1
1
  # Reqcord
2
2
 
3
3
  [![CI](https://github.com/ahmetsaridogan/reqcord/actions/workflows/ci.yml/badge.svg)](https://github.com/ahmetsaridogan/reqcord/actions/workflows/ci.yml)
4
+ [![Gem](https://img.shields.io/gem/v/reqcord)](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
- **Turn your Rails integration tests into living API documentation.**
6
-
7
- Reqcord observes real HTTP requests and responses executed by your Rails test suite and converts them into static, readable API documentation.
53
+ ```ruby
54
+ # Gemfile
55
+ group :development, :test do
56
+ gem "reqcord"
57
+ end
58
+ ```
8
59
 
9
- Instead of maintaining API documentation separately from your tests, Reqcord uses the requests your application already executes as the source of truth.
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
- Rails Routes
13
- +
14
- Minitest Integration Tests
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
- ## Why Reqcord?
28
-
29
- API documentation tends to drift away from the application it describes.
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
- A new `422` response appears.
76
+ Optionally, browse it inside the app with Scalar:
38
77
 
39
- The tests are updated, but the documentation is forgotten.
40
-
41
- Reqcord takes a different approach:
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
- ## Example
83
+ ## What you get
56
84
 
57
- Given an existing Rails integration test:
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
- customer: {
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
- Reqcord captures the request and response while the test executes.
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 tables are inferred from the requests the application
172
- **accepted**: two passing tests sent `"active"` and `"passive"`, a third sent
173
- `"inactive"` and got a `422`, so the documentation lists the two values that
174
- work and keeps the rejection only as a response example. The same run also
175
- writes a runnable `curl/api/v2/customers/create.sh`, a Postman collection
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
- Two runnable examples live in [`examples/`](examples):
137
+ ## Documentation
183
138
 
184
- | Example | Test framework | What it shows |
185
- | --- | --- | --- |
186
- | [`examples/test-app`](examples/test-app) | Minitest | three small resources: auth, closed value sets, PATCH/PUT folding, a member action |
187
- | [`examples/spec-app`](examples/spec-app) | RSpec | the same API, documented from request specs |
188
- | [`examples/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 |
189
- | [`examples/complex-spec-app`](examples/complex-spec-app) | RSpec | the store API from request specs |
190
-
191
- Each one ships the documentation it generates, so you can read the output
192
- before running anything. [`examples/reqcord.yml`](examples/reqcord.yml) is an
193
- annotated configuration file.
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
- Reqcord separates **capturing API behavior** from **rendering documentation**.
151
+ ## Examples
198
152
 
199
- ```text
200
- Rails Routes
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
- The internal dataset is framework-independent and output-independent.
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
- This allows Reqcord to support additional test frameworks and documentation formats without coupling them together.
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
- | Test frameworks | Minitest integration tests, RSpec request specs |
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
- Generate documentation for the entire API:
174
+ Every Ruby × Rails pair Rails itself supports runs in CI.
306
175
 
307
- ```bash
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
- Combine filters:
330
-
331
- ```bash
332
- bin/rails reqcord:generate RESOURCE=customers VERSION=v2
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
- Reqcord is available as open source under the terms of the MIT License.
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.