reqcord 0.1.0 → 0.1.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +252 -95
  6. data/Rakefile +13 -0
  7. data/docs/configuration.md +319 -0
  8. data/examples/reqcord.yml +58 -0
  9. data/gemfiles/rails_7.1.gemfile +13 -0
  10. data/gemfiles/rails_7.2.gemfile +13 -0
  11. data/gemfiles/rails_8.0.gemfile +13 -0
  12. data/gemfiles/rails_8.1.gemfile +13 -0
  13. data/lib/reqcord/capture/collector.rb +31 -0
  14. data/lib/reqcord/capture/integration_patch.rb +209 -0
  15. data/lib/reqcord/capture/minitest_context.rb +34 -0
  16. data/lib/reqcord/capture/rspec_context.rb +42 -0
  17. data/lib/reqcord/capture/test_context.rb +25 -0
  18. data/lib/reqcord/capture.rb +19 -0
  19. data/lib/reqcord/configuration.rb +198 -0
  20. data/lib/reqcord/dataset.rb +176 -0
  21. data/lib/reqcord/endpoint.rb +263 -0
  22. data/lib/reqcord/errors.rb +9 -0
  23. data/lib/reqcord/exporters/curl.rb +68 -0
  24. data/lib/reqcord/exporters/markdown.rb +295 -0
  25. data/lib/reqcord/exporters/postman.rb +206 -0
  26. data/lib/reqcord/exporters.rb +32 -0
  27. data/lib/reqcord/generator.rb +364 -0
  28. data/lib/reqcord/railtie.rb +51 -0
  29. data/lib/reqcord/renderers/curl.rb +56 -0
  30. data/lib/reqcord/renderers/payload.rb +69 -0
  31. data/lib/reqcord/request_example.rb +104 -0
  32. data/lib/reqcord/response_example.rb +72 -0
  33. data/lib/reqcord/route_collector.rb +242 -0
  34. data/lib/reqcord/sanitizers/sanitizer.rb +140 -0
  35. data/lib/reqcord/schema.rb +187 -0
  36. data/lib/reqcord/support.rb +58 -0
  37. data/lib/reqcord/version.rb +5 -0
  38. data/lib/reqcord.rb +78 -0
  39. data/lib/tasks/reqcord.rake +99 -0
  40. data/reqcord.gemspec +60 -0
  41. metadata +165 -3
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Reqcord
2
2
 
3
+ [![CI](https://github.com/ahmetsaridogan/reqcord/actions/workflows/ci.yml/badge.svg)](https://github.com/ahmetsaridogan/reqcord/actions/workflows/ci.yml)
4
+
3
5
  **Turn your Rails integration tests into living API documentation.**
4
6
 
5
7
  Reqcord observes real HTTP requests and responses executed by your Rails test suite and converts them into static, readable API documentation.
@@ -19,9 +21,7 @@ Minitest Integration Tests
19
21
 
20
22
  Canonical Dataset
21
23
 
22
- Markdown API Docs
23
-
24
- cURL Examples
24
+ Markdown · cURL · Postman
25
25
  ```
26
26
 
27
27
  ## Why Reqcord?
@@ -92,13 +92,22 @@ It can generate documentation such as:
92
92
  | X-Account-Id | `{{account_id}}` |
93
93
  | Content-Type | `application/json` |
94
94
 
95
- ## Request Body
95
+ ## Body Parameters
96
+
97
+ | Field | Type | Required | Values |
98
+ | --- | --- | --- | --- |
99
+ | `customer.name` | string | yes | `"John Doe"` |
100
+ | `customer.email` | string | yes | `"john@example.com"` |
101
+ | `customer.status` | string | yes | `"active"` \| `"passive"` |
102
+
103
+ ## Example Request
96
104
 
97
105
  ```json
98
106
  {
99
107
  "customer": {
100
108
  "name": "John Doe",
101
- "email": "john@example.com"
109
+ "email": "john@example.com",
110
+ "status": "active"
102
111
  }
103
112
  }
104
113
  ```
@@ -107,27 +116,34 @@ It can generate documentation such as:
107
116
 
108
117
  ```bash
109
118
  curl --request POST \
110
- --url "{{base_url}}/api/v2/customers" \
119
+ --url "http://localhost:3000/api/v2/customers" \
111
120
  --header "Authorization: Bearer {{token}}" \
112
121
  --header "X-Account-Id: {{account_id}}" \
113
122
  --header "Content-Type: application/json" \
114
123
  --data '{
115
- "customer": {
116
- "name": "John Doe",
117
- "email": "john@example.com"
118
- }
119
- }'
124
+ "customer": {
125
+ "name": "John Doe",
126
+ "email": "john@example.com",
127
+ "status": "active"
128
+ }
129
+ }'
120
130
  ```
121
131
 
122
132
  ## Responses
123
133
 
124
134
  ### 201 Created
125
135
 
136
+ #### Fields
137
+
138
+ | Field | Type | Required | Values |
139
+ | --- | --- | --- | --- |
140
+ | `id` | integer | yes | `42` |
141
+ | `name` | string | yes | `"John Doe"` |
142
+
126
143
  ```json
127
144
  {
128
145
  "id": 42,
129
- "name": "John Doe",
130
- "email": "john@example.com"
146
+ "name": "John Doe"
131
147
  }
132
148
  ```
133
149
 
@@ -139,7 +155,7 @@ curl --request POST \
139
155
  }
140
156
  ```
141
157
 
142
- ### 422 Unprocessable Entity
158
+ ### 422 Unprocessable Content
143
159
 
144
160
  ```json
145
161
  {
@@ -152,6 +168,28 @@ curl --request POST \
152
168
  ```
153
169
  ````
154
170
 
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` and a Postman collection
176
+ with this request and its three saved responses.
177
+
178
+ ## Examples
179
+
180
+ Two runnable examples live in [`examples/`](examples):
181
+
182
+ | Example | Test framework | What it shows |
183
+ | --- | --- | --- |
184
+ | [`examples/test-app`](examples/test-app) | Minitest | three small resources: auth, closed value sets, PATCH/PUT folding, a member action |
185
+ | [`examples/spec-app`](examples/spec-app) | RSpec | the same API, documented from request specs |
186
+ | [`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 |
187
+ | [`examples/complex-spec-app`](examples/complex-spec-app) | RSpec | the store API from request specs |
188
+
189
+ Each one ships the documentation it generates, so you can read the output
190
+ before running anything. [`examples/reqcord.yml`](examples/reqcord.yml) is an
191
+ annotated configuration file.
192
+
155
193
  ## Core Idea
156
194
 
157
195
  Reqcord separates **capturing API behavior** from **rendering documentation**.
@@ -167,18 +205,26 @@ Minitest ──────► Test Adapter
167
205
 
168
206
  Reqcord Dataset
169
207
 
170
- ┌─────────┴─────────┐
171
-
172
- Markdown JSON
173
-
174
-
175
- cURL
208
+ ┌────────────┼────────────┐
209
+
210
+ Markdown cURL Postman
176
211
  ```
177
212
 
178
213
  The internal dataset is framework-independent and output-independent.
179
214
 
180
215
  This allows Reqcord to support additional test frameworks and documentation formats without coupling them together.
181
216
 
217
+ ## Supported versions
218
+
219
+ | | |
220
+ | --- | --- |
221
+ | Ruby | 3.2, 3.3, 3.4 |
222
+ | Rails | 7.1, 7.2, 8.0, 8.1 |
223
+ | Test frameworks | Minitest integration tests, RSpec request specs |
224
+
225
+ Every Ruby × Rails pair that Rails itself supports runs in CI
226
+ (`gemfiles/rails_*.gemfile`).
227
+
182
228
  ## Installation
183
229
 
184
230
  Add Reqcord to the development and test groups:
@@ -211,13 +257,15 @@ docs/
211
257
 
212
258
  ## Configuration
213
259
 
214
- Reqcord reads its configuration from `reqcord.yml` in the project root.
260
+ Reqcord reads its configuration from `reqcord.yml` in the project root. Every
261
+ key, default and environment override is described in
262
+ [docs/configuration.md](docs/configuration.md); the short version:
215
263
 
216
264
  ```yaml
217
265
  version: 1
218
266
 
219
267
  test:
220
- framework: minitest
268
+ framework: minitest # or: rspec
221
269
 
222
270
  routes:
223
271
  prefix: /api
@@ -226,7 +274,9 @@ output:
226
274
  directory: docs/api
227
275
 
228
276
  exporters:
277
+ - curl
229
278
  - markdown
279
+ - postman
230
280
 
231
281
  variables:
232
282
  base_url: http://localhost:3000
@@ -279,28 +329,122 @@ Combine filters:
279
329
  bin/rails reqcord:generate RESOURCE=customers VERSION=v2
280
330
  ```
281
331
 
332
+ `reqcord:generate` runs the test suite itself, in a subprocess, with capture
333
+ enabled:
334
+
335
+ ```text
336
+ bin/rails reqcord:generate
337
+ |
338
+ +-- collects the application's routes
339
+ |
340
+ +-- runs `test.command` with REQCORD_CAPTURE=1
341
+ | |
342
+ | +-- each request appends a JSON line to the capture file
343
+ |
344
+ +-- reads the capture file, sanitizes, writes the documentation
345
+ ```
346
+
347
+ Because capture is driven by `REQCORD_CAPTURE` and `REQCORD_CAPTURE_FILE`, an
348
+ ordinary `bin/rails test` patches nothing and writes nothing. The capture file
349
+ is append-only and locked per write, so parallel test workers can share it.
350
+
351
+ Point Reqcord at the tests that exercise the API — a directory is enough, it
352
+ picks the runner (`bin/rails test`, `rspec`, or a plain Ruby runner when the
353
+ project has no `bin/rails`):
354
+
355
+ ```yaml
356
+ test:
357
+ framework: minitest
358
+ paths:
359
+ - test/integration
360
+ - test/api
361
+ ```
362
+
363
+ Or spell the command out; it wins over `paths`, and globs are expanded:
364
+
365
+ ```yaml
366
+ test:
367
+ command: bin/rails test test/integration test/api/*_test.rb
368
+ ```
369
+
370
+ The run ends with a reconciliation of the whole route table, so nothing can
371
+ go missing quietly:
372
+
373
+ ```text
374
+ [reqcord] captured 87 request(s), 85 matched a documented route
375
+ [reqcord] captured a successful 2xx request for 15 of 16 endpoint(s)
376
+ [reqcord] routes: 18 = 15 documented + 1 uncovered + 2 skipped
377
+ [reqcord] skipped 2 route(s) that cannot be documented: 1 redirect, 1 mount
378
+ ```
379
+
380
+ Every route is in exactly one bucket: *documented* (a test got a `2xx`),
381
+ *uncovered* (listed in the index, no page), or *skipped* with its reason.
382
+
282
383
  ## Generated Files
283
384
 
284
- A typical output looks like:
385
+ Directories follow the controller path, so `admin/customers` and
386
+ `api/v2/customers` never collide:
285
387
 
286
388
  ```text
287
389
  docs/api/
288
390
  ├── dataset.json
289
391
  ├── README.md
290
- ├── customers/
392
+ ├── api/v2/customers/
291
393
  │ ├── index.md
292
394
  │ ├── create.md
293
395
  │ ├── show.md
294
- ├── update.md
295
- │ └── destroy.md
296
- └── surveys/
297
- ├── index.md
298
- └── create.md
396
+ └── update.md
397
+ ├── api/v2/surveys/
398
+ │ ├── index.md
399
+ │ └── list.md
400
+ ├── curl/
401
+ │ └── api/v2/customers/
402
+ │ ├── create.sh
403
+ │ └── show.sh
404
+ └── postman/
405
+ └── collection.json
299
406
  ```
300
407
 
301
- `dataset.json` contains Reqcord's normalized representation of the captured API.
408
+ `dataset.json` contains Reqcord's normalized representation of the captured
409
+ API; every exporter reads that and nothing else.
410
+
411
+ ## Route coverage
302
412
 
303
- Markdown files are generated from that dataset.
413
+ The documented surface is the route table, not only `resources`. These all
414
+ become endpoints:
415
+
416
+ | Route | Documented as |
417
+ | --- | --- |
418
+ | `resources :customers` | one endpoint per action |
419
+ | `resource :cart` | `GET /cart`, `PATCH /cart` (also `PUT`) |
420
+ | `match "/echo", via: [:get, :post]` | `GET /echo` and `POST /echo` |
421
+ | `match "/anything", via: :all` | one endpoint per verb the tests used |
422
+ | `root to: "home#index"` | `GET /`, titled "Home" |
423
+ | `get "/items(/:id)"` | one endpoint, `:id` optional |
424
+ | `get "/files/*path"` | `path` as a path parameter |
425
+ | `mount Billing => "/billing"` | the engine's own routes, under `/billing` |
426
+ | `namespace :admin { resources :customers }` | `admin/customers/`, apart from `api/v2/customers/` |
427
+
428
+ `redirect(...)` routes and plain Rack mounts cannot be documented from a test;
429
+ they are counted as *skipped* in the report rather than dropped.
430
+
431
+ ## Postman and Hoppscotch
432
+
433
+ `postman/collection.json` is a Postman Collection v2.1:
434
+
435
+ * one folder per controller namespace (`Api › V2 › Customers`),
436
+ * one request per documented endpoint, built from the successful captured
437
+ example — JSON bodies as `raw`, form bodies as `urlencoded`,
438
+ * every captured status saved as a response example on that request,
439
+ * collection variables for `base_url` and every placeholder the sanitizer
440
+ wrote (`{{token}}`, `{{api_key}}` …) — Postman's variable syntax is the
441
+ same, so the collection is usable as soon as the variables are filled in,
442
+ * `Authorization: Bearer {{token}}` lifted to collection-level bearer auth;
443
+ requests that were made without credentials are marked `noauth`, so they
444
+ replay exactly as their tests did.
445
+
446
+ Hoppscotch imports Postman v2.1 collections directly: *Import → Postman* and
447
+ point it at the same file.
304
448
 
305
449
  ## Request Capture
306
450
 
@@ -365,18 +509,18 @@ Responses
365
509
  └── 422 Unprocessable Entity
366
510
  ```
367
511
 
368
- Multiple examples for the same status code are also preserved.
369
-
370
- For example:
512
+ Every distinct body captured for a status is kept in `dataset.json`, and the
513
+ fields of a response are inferred from all of them:
371
514
 
372
515
  ```text
373
- 422 Unprocessable Entity
516
+ 422 Unprocessable Content
374
517
  ├── Email already exists
375
518
  ├── Name is required
376
519
  └── Invalid phone number
377
520
  ```
378
521
 
379
- Reqcord does not overwrite one `422` example with another.
522
+ The Markdown page shows one example body per status plus the inferred field
523
+ table; Reqcord does not overwrite one `422` example with another.
380
524
 
381
525
  ## Sanitization
382
526
 
@@ -408,7 +552,15 @@ Authorization: Bearer {{token}}
408
552
 
409
553
  Sensitive headers such as authorization credentials, cookies and API keys are treated specially by Reqcord.
410
554
 
411
- Request and response body sanitization will follow the same principle.
555
+ Request and response bodies follow the same principle, matched by key at any
556
+ depth:
557
+
558
+ ```yaml
559
+ sanitize:
560
+ body:
561
+ password: "{{password}}"
562
+ access_token: "{{token}}"
563
+ ```
412
564
 
413
565
  ## Canonical Dataset
414
566
 
@@ -430,45 +582,43 @@ A simplified endpoint representation looks like:
430
582
 
431
583
  ```json
432
584
  {
585
+ "name": "Create Customer",
433
586
  "method": "POST",
434
587
  "path": "/api/v2/customers",
435
- "request_examples": [
436
- {
437
- "headers": {
438
- "Authorization": "Bearer {{token}}"
439
- },
440
- "body": {
441
- "customer": {
442
- "name": "John Doe"
443
- }
444
- }
445
- }
446
- ],
447
- "response_examples": [
588
+ "controller": "api/v2/customers",
589
+ "action": "create",
590
+ "parameters": {
591
+ "path": [],
592
+ "query": [],
593
+ "body": [
594
+ { "path": "customer.name", "type": "string", "required": true, "values": ["John Doe"] },
595
+ { "path": "customer.status", "type": "string", "required": true, "values": ["active", "passive"] }
596
+ ]
597
+ },
598
+ "responses": [
448
599
  {
449
- "name": "Created",
450
600
  "status": 201,
451
- "body": {
452
- "id": 42,
453
- "name": "John Doe"
454
- }
601
+ "schema": [
602
+ { "path": "id", "type": "integer", "required": true, "values": [42] }
603
+ ],
604
+ "example": { "id": 42, "name": "John Doe" }
455
605
  },
456
- {
457
- "name": "Unauthorized",
458
- "status": 401,
459
- "body": {
460
- "error": "Unauthorized"
461
- }
462
- }
463
- ]
606
+ { "status": 401, "schema": [ { "path": "error", "type": "string", "required": true, "values": ["Unauthorized"] } ], "example": { "error": "Unauthorized" } }
607
+ ],
608
+ "request_examples": [ "… every captured request, sanitized" ],
609
+ "response_examples": [ "… every captured response, sanitized" ]
464
610
  }
465
611
  ```
466
612
 
613
+ `parameters` and `responses[].schema` are inferred only from requests the
614
+ application accepted; `request_examples` keeps everything that was captured.
615
+ Routes no test reached are listed separately under `uncovered_routes`.
616
+
467
617
  Every dataset contains a schema version so the internal format can evolve safely.
468
618
 
469
619
  ```json
470
620
  {
471
- "schema_version": 1
621
+ "schema_version": 2
472
622
  }
473
623
  ```
474
624
 
@@ -478,26 +628,33 @@ Every dataset contains a schema version so the internal format can evolve safely
478
628
  Reqcord
479
629
  ├── Configuration
480
630
  ├── Dataset
481
- │ ├── Resource
631
+ │ ├── Resource (one controller path, nested directories/folders)
482
632
  │ ├── Endpoint
483
633
  │ ├── RequestExample
484
- └── ResponseExample
634
+ ├── ResponseExample
635
+ │ └── Schema (fields, types, required, closed value sets)
636
+
637
+ ├── RouteCollector (every route kind, engines walked, skips counted)
485
638
 
486
- ├── RouteCollector
639
+ ├── Capture
640
+ │ ├── Collector (NDJSON, one line per exchange)
641
+ │ ├── TestContext
642
+ │ ├── MinitestContext / RSpecContext
643
+ │ └── IntegrationPatch
487
644
 
488
- ├── TestAdapters
489
- │ └── Minitest
645
+ ├── Generator (run tests → dataset → exporters → report)
490
646
 
491
647
  ├── Sanitizers
492
- ├── Headers
493
- │ ├── RequestBody
494
- │ └── ResponseBody
648
+ └── Sanitizer (headers and bodies)
495
649
 
496
650
  ├── Renderers
651
+ │ ├── Payload (JSON vs form, nested query flattening)
497
652
  │ └── Curl
498
653
 
499
654
  └── Exporters
500
- └── Markdown
655
+ ├── Markdown
656
+ ├── Curl (one .sh per endpoint)
657
+ └── Postman (Collection v2.1, also for Hoppscotch)
501
658
  ```
502
659
 
503
660
  Test adapters are responsible only for converting test execution into Reqcord's canonical model.
@@ -508,9 +665,9 @@ Exporters know nothing about Minitest or Rails test internals.
508
665
  Minitest ──┐
509
666
 
510
667
  RSpec ─────┼──► Dataset ──► Markdown
511
- │ ├─► OpenAPI
512
- Other ─────┘ ├─► Postman
513
- └─► ...
668
+ │ ├─► cURL
669
+ Other ─────┘ ├─► Postman (→ Hoppscotch)
670
+ └─► OpenAPI (0.2)
514
671
  ```
515
672
 
516
673
  ## v0.1 Scope
@@ -520,28 +677,28 @@ The first Reqcord release focuses on proving the capture pipeline.
520
677
  ### Included
521
678
 
522
679
  * Rails 8
523
- * Minitest integration/request tests
680
+ * Minitest integration tests
681
+ * RSpec request specs
524
682
  * Rails route discovery
525
683
  * `reqcord.yml`
526
684
  * Request capture
527
685
  * Response capture
528
686
  * Multiple response scenarios
529
687
  * Sensitive data sanitization
530
- * Canonical `dataset.json`
688
+ * Canonical `dataset.json` with inferred request parameters and response fields
531
689
  * Markdown documentation
532
- * Generated cURL requests
690
+ * Generated cURL requests (in the Markdown and as runnable `.sh` files)
691
+ * Postman Collection v2.1 (imports into Hoppscotch as well)
692
+ * The whole route table: custom actions, `match via:`, `via: :all`,
693
+ singular resources, optional segments and globs, mounted engines
533
694
  * Resource filtering
534
695
  * API version filtering
535
696
 
536
697
  ### Not included yet
537
698
 
538
- * RSpec adapter
539
699
  * OpenAPI generation
540
700
  * Scalar integration
541
- * Postman collections
542
- * Hoppscotch collections
543
701
  * Multipart requests
544
- * Advanced schema inference
545
702
  * CI documentation drift detection
546
703
 
547
704
  These features belong to later releases rather than expanding the initial scope.
@@ -568,22 +725,13 @@ http://localhost:3000/api-docs
568
725
 
569
726
  ### v0.3
570
727
 
571
- RSpec request spec adapter.
572
-
573
- Both test frameworks will produce the exact same Reqcord dataset:
574
-
575
- ```text
576
- Minitest ─┐
577
- ├──► Reqcord Dataset
578
- RSpec ────┘
579
- ```
728
+ Rack::Test capture, so frameworks other than Rails (Sinatra, Roda, Hanami) can
729
+ be documented from the same dataset.
580
730
 
581
731
  ### Future
582
732
 
583
733
  Potential exporters and integrations include:
584
734
 
585
- * Postman
586
- * Hoppscotch
587
735
  * Bruno
588
736
  * Insomnia
589
737
  * `llms.txt`
@@ -619,10 +767,19 @@ Reqcord is currently in early development.
619
767
 
620
768
  The initial goal is intentionally narrow:
621
769
 
622
- > Capture real Rails API requests and responses from Minitest and generate accurate, sanitized Markdown documentation with executable cURL examples.
770
+ > Capture real Rails API requests and responses from the test suite and generate accurate, sanitized Markdown documentation, executable cURL examples and a Postman collection — without the developer writing any of them by hand.
623
771
 
624
772
  Once that pipeline is reliable, additional adapters and exporters can be built on top of the same dataset.
625
773
 
626
774
  ## License
627
775
 
628
776
  Reqcord is available as open source under the terms of the MIT License.
777
+
778
+ ## cURL source of truth
779
+
780
+ Reqcord does not invent request payloads. For Rails integration tests, the
781
+ arguments passed to `get`, `post`, `put`, `patch`, and `delete` are captured at
782
+ runtime. Generated cURL commands use a successful `2xx` test case whenever one
783
+ exists, including its concrete URL, request headers, query parameters, and
784
+ payload. Error-case payloads remain available as examples but do not replace
785
+ the canonical successful request.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |task|
7
+ task.libs << "test"
8
+ task.libs << "lib"
9
+
10
+ task.pattern = "test/**/*_test.rb"
11
+ end
12
+
13
+ task default: :test