reqcord 0.1.0 → 0.1.1

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/CHANGELOG.md +77 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +171 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +239 -95
  8. data/Rakefile +13 -0
  9. data/docs/configuration.md +319 -0
  10. data/examples/complex-spec-app/.rspec +1 -0
  11. data/examples/complex-spec-app/README.md +21 -0
  12. data/examples/complex-spec-app/app.rb +363 -0
  13. data/examples/complex-spec-app/docs/api/README.md +104 -0
  14. data/examples/complex-spec-app/docs/api/api/v1/admin/products/create.md +117 -0
  15. data/examples/complex-spec-app/docs/api/api/v1/admin/products/destroy.md +51 -0
  16. data/examples/complex-spec-app/docs/api/api/v1/admin/products/index.md +11 -0
  17. data/examples/complex-spec-app/docs/api/api/v1/admin/products/list.md +106 -0
  18. data/examples/complex-spec-app/docs/api/api/v1/auth/index.md +9 -0
  19. data/examples/complex-spec-app/docs/api/api/v1/auth/login.md +76 -0
  20. data/examples/complex-spec-app/docs/api/api/v1/cart-items/create.md +91 -0
  21. data/examples/complex-spec-app/docs/api/api/v1/cart-items/destroy.md +51 -0
  22. data/examples/complex-spec-app/docs/api/api/v1/cart-items/index.md +10 -0
  23. data/examples/complex-spec-app/docs/api/api/v1/carts/checkout.md +102 -0
  24. data/examples/complex-spec-app/docs/api/api/v1/carts/index.md +10 -0
  25. data/examples/complex-spec-app/docs/api/api/v1/carts/show.md +73 -0
  26. data/examples/complex-spec-app/docs/api/api/v1/home/index.md +9 -0
  27. data/examples/complex-spec-app/docs/api/api/v1/home/list.md +47 -0
  28. data/examples/complex-spec-app/docs/api/api/v1/notes/create.md +94 -0
  29. data/examples/complex-spec-app/docs/api/api/v1/notes/index.md +10 -0
  30. data/examples/complex-spec-app/docs/api/api/v1/notes/list.md +61 -0
  31. data/examples/complex-spec-app/docs/api/api/v1/orders/cancel.md +69 -0
  32. data/examples/complex-spec-app/docs/api/api/v1/orders/create.md +150 -0
  33. data/examples/complex-spec-app/docs/api/api/v1/orders/destroy.md +37 -0
  34. data/examples/complex-spec-app/docs/api/api/v1/orders/index.md +13 -0
  35. data/examples/complex-spec-app/docs/api/api/v1/orders/list.md +78 -0
  36. data/examples/complex-spec-app/docs/api/api/v1/orders/show.md +83 -0
  37. data/examples/complex-spec-app/docs/api/api/v1/products/index.md +11 -0
  38. data/examples/complex-spec-app/docs/api/api/v1/products/list.md +83 -0
  39. data/examples/complex-spec-app/docs/api/api/v1/products/search.md +88 -0
  40. data/examples/complex-spec-app/docs/api/api/v1/products/show.md +71 -0
  41. data/examples/complex-spec-app/docs/api/api/v1/profiles/index.md +10 -0
  42. data/examples/complex-spec-app/docs/api/api/v1/profiles/show.md +61 -0
  43. data/examples/complex-spec-app/docs/api/api/v1/profiles/update.md +91 -0
  44. data/examples/complex-spec-app/docs/api/api/v2/products/index.md +9 -0
  45. data/examples/complex-spec-app/docs/api/api/v2/products/list.md +77 -0
  46. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
  47. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
  48. data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
  49. data/examples/complex-spec-app/docs/api/curl/api/v1/auth/login.sh +9 -0
  50. data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
  51. data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
  52. data/examples/complex-spec-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
  53. data/examples/complex-spec-app/docs/api/curl/api/v1/carts/show.sh +9 -0
  54. data/examples/complex-spec-app/docs/api/curl/api/v1/home/list.sh +8 -0
  55. data/examples/complex-spec-app/docs/api/curl/api/v1/notes/create.sh +15 -0
  56. data/examples/complex-spec-app/docs/api/curl/api/v1/notes/list.sh +9 -0
  57. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
  58. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/create.sh +29 -0
  59. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
  60. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/list.sh +9 -0
  61. data/examples/complex-spec-app/docs/api/curl/api/v1/orders/show.sh +9 -0
  62. data/examples/complex-spec-app/docs/api/curl/api/v1/products/list.sh +8 -0
  63. data/examples/complex-spec-app/docs/api/curl/api/v1/products/search.sh +8 -0
  64. data/examples/complex-spec-app/docs/api/curl/api/v1/products/show.sh +8 -0
  65. data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
  66. data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
  67. data/examples/complex-spec-app/docs/api/curl/api/v2/products/list.sh +8 -0
  68. data/examples/complex-spec-app/docs/api/dataset.json +5116 -0
  69. data/examples/complex-spec-app/docs/api/postman/collection.json +2519 -0
  70. data/examples/complex-spec-app/generate.rb +14 -0
  71. data/examples/complex-spec-app/reqcord.yml +28 -0
  72. data/examples/complex-spec-app/spec/requests/admin_spec.rb +67 -0
  73. data/examples/complex-spec-app/spec/requests/cart_spec.rb +61 -0
  74. data/examples/complex-spec-app/spec/requests/home_and_auth_spec.rb +49 -0
  75. data/examples/complex-spec-app/spec/requests/orders_spec.rb +124 -0
  76. data/examples/complex-spec-app/spec/requests/products_spec.rb +65 -0
  77. data/examples/complex-spec-app/spec/spec_helper.rb +17 -0
  78. data/examples/complex-test-app/README.md +40 -0
  79. data/examples/complex-test-app/app.rb +384 -0
  80. data/examples/complex-test-app/docs/api/README.md +104 -0
  81. data/examples/complex-test-app/docs/api/api/v1/admin/products/create.md +117 -0
  82. data/examples/complex-test-app/docs/api/api/v1/admin/products/destroy.md +51 -0
  83. data/examples/complex-test-app/docs/api/api/v1/admin/products/index.md +11 -0
  84. data/examples/complex-test-app/docs/api/api/v1/admin/products/list.md +106 -0
  85. data/examples/complex-test-app/docs/api/api/v1/auth/index.md +9 -0
  86. data/examples/complex-test-app/docs/api/api/v1/auth/login.md +76 -0
  87. data/examples/complex-test-app/docs/api/api/v1/cart-items/create.md +91 -0
  88. data/examples/complex-test-app/docs/api/api/v1/cart-items/destroy.md +51 -0
  89. data/examples/complex-test-app/docs/api/api/v1/cart-items/index.md +10 -0
  90. data/examples/complex-test-app/docs/api/api/v1/carts/checkout.md +102 -0
  91. data/examples/complex-test-app/docs/api/api/v1/carts/index.md +10 -0
  92. data/examples/complex-test-app/docs/api/api/v1/carts/show.md +73 -0
  93. data/examples/complex-test-app/docs/api/api/v1/home/index.md +9 -0
  94. data/examples/complex-test-app/docs/api/api/v1/home/list.md +47 -0
  95. data/examples/complex-test-app/docs/api/api/v1/notes/create.md +94 -0
  96. data/examples/complex-test-app/docs/api/api/v1/notes/index.md +10 -0
  97. data/examples/complex-test-app/docs/api/api/v1/notes/list.md +61 -0
  98. data/examples/complex-test-app/docs/api/api/v1/orders/cancel.md +69 -0
  99. data/examples/complex-test-app/docs/api/api/v1/orders/create.md +150 -0
  100. data/examples/complex-test-app/docs/api/api/v1/orders/destroy.md +37 -0
  101. data/examples/complex-test-app/docs/api/api/v1/orders/index.md +13 -0
  102. data/examples/complex-test-app/docs/api/api/v1/orders/list.md +71 -0
  103. data/examples/complex-test-app/docs/api/api/v1/orders/show.md +83 -0
  104. data/examples/complex-test-app/docs/api/api/v1/products/index.md +11 -0
  105. data/examples/complex-test-app/docs/api/api/v1/products/list.md +83 -0
  106. data/examples/complex-test-app/docs/api/api/v1/products/search.md +88 -0
  107. data/examples/complex-test-app/docs/api/api/v1/products/show.md +71 -0
  108. data/examples/complex-test-app/docs/api/api/v1/profiles/index.md +10 -0
  109. data/examples/complex-test-app/docs/api/api/v1/profiles/show.md +61 -0
  110. data/examples/complex-test-app/docs/api/api/v1/profiles/update.md +91 -0
  111. data/examples/complex-test-app/docs/api/api/v2/products/index.md +9 -0
  112. data/examples/complex-test-app/docs/api/api/v2/products/list.md +77 -0
  113. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
  114. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
  115. data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
  116. data/examples/complex-test-app/docs/api/curl/api/v1/auth/login.sh +9 -0
  117. data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
  118. data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
  119. data/examples/complex-test-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
  120. data/examples/complex-test-app/docs/api/curl/api/v1/carts/show.sh +9 -0
  121. data/examples/complex-test-app/docs/api/curl/api/v1/home/list.sh +8 -0
  122. data/examples/complex-test-app/docs/api/curl/api/v1/notes/create.sh +15 -0
  123. data/examples/complex-test-app/docs/api/curl/api/v1/notes/list.sh +9 -0
  124. data/examples/complex-test-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
  125. data/examples/complex-test-app/docs/api/curl/api/v1/orders/create.sh +29 -0
  126. data/examples/complex-test-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
  127. data/examples/complex-test-app/docs/api/curl/api/v1/orders/list.sh +9 -0
  128. data/examples/complex-test-app/docs/api/curl/api/v1/orders/show.sh +9 -0
  129. data/examples/complex-test-app/docs/api/curl/api/v1/products/list.sh +8 -0
  130. data/examples/complex-test-app/docs/api/curl/api/v1/products/search.sh +8 -0
  131. data/examples/complex-test-app/docs/api/curl/api/v1/products/show.sh +8 -0
  132. data/examples/complex-test-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
  133. data/examples/complex-test-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
  134. data/examples/complex-test-app/docs/api/curl/api/v2/products/list.sh +8 -0
  135. data/examples/complex-test-app/docs/api/dataset.json +5109 -0
  136. data/examples/complex-test-app/docs/api/postman/collection.json +2568 -0
  137. data/examples/complex-test-app/generate.rb +14 -0
  138. data/examples/complex-test-app/reqcord.yml +28 -0
  139. data/examples/complex-test-app/test/integration/admin_test.rb +70 -0
  140. data/examples/complex-test-app/test/integration/cart_test.rb +64 -0
  141. data/examples/complex-test-app/test/integration/home_and_auth_test.rb +53 -0
  142. data/examples/complex-test-app/test/integration/orders_test.rb +127 -0
  143. data/examples/complex-test-app/test/integration/products_test.rb +66 -0
  144. data/examples/reqcord.yml +58 -0
  145. data/examples/spec-app/.rspec +1 -0
  146. data/examples/spec-app/README.md +36 -0
  147. data/examples/spec-app/app.rb +209 -0
  148. data/examples/spec-app/docs/api/README.md +45 -0
  149. data/examples/spec-app/docs/api/api/v1/customers/create.md +95 -0
  150. data/examples/spec-app/docs/api/api/v1/customers/index.md +11 -0
  151. data/examples/spec-app/docs/api/api/v1/customers/list.md +67 -0
  152. data/examples/spec-app/docs/api/api/v1/customers/show.md +65 -0
  153. data/examples/spec-app/docs/api/api/v1/tasks/complete.md +51 -0
  154. data/examples/spec-app/docs/api/api/v1/tasks/create.md +90 -0
  155. data/examples/spec-app/docs/api/api/v1/tasks/destroy.md +35 -0
  156. data/examples/spec-app/docs/api/api/v1/tasks/index.md +14 -0
  157. data/examples/spec-app/docs/api/api/v1/tasks/list.md +65 -0
  158. data/examples/spec-app/docs/api/api/v1/tasks/show.md +65 -0
  159. data/examples/spec-app/docs/api/api/v1/tasks/update.md +74 -0
  160. data/examples/spec-app/docs/api/api/v1/users/create.md +95 -0
  161. data/examples/spec-app/docs/api/api/v1/users/index.md +11 -0
  162. data/examples/spec-app/docs/api/api/v1/users/list.md +67 -0
  163. data/examples/spec-app/docs/api/api/v1/users/show.md +65 -0
  164. data/examples/spec-app/docs/api/curl/api/v1/customers/create.sh +17 -0
  165. data/examples/spec-app/docs/api/curl/api/v1/customers/list.sh +9 -0
  166. data/examples/spec-app/docs/api/curl/api/v1/customers/show.sh +9 -0
  167. data/examples/spec-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
  168. data/examples/spec-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
  169. data/examples/spec-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
  170. data/examples/spec-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
  171. data/examples/spec-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
  172. data/examples/spec-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
  173. data/examples/spec-app/docs/api/curl/api/v1/users/create.sh +17 -0
  174. data/examples/spec-app/docs/api/curl/api/v1/users/list.sh +9 -0
  175. data/examples/spec-app/docs/api/curl/api/v1/users/show.sh +9 -0
  176. data/examples/spec-app/docs/api/dataset.json +2402 -0
  177. data/examples/spec-app/docs/api/postman/collection.json +1290 -0
  178. data/examples/spec-app/generate.rb +14 -0
  179. data/examples/spec-app/reqcord.yml +27 -0
  180. data/examples/spec-app/spec/requests/customers_spec.rb +71 -0
  181. data/examples/spec-app/spec/requests/tasks_spec.rb +78 -0
  182. data/examples/spec-app/spec/requests/users_spec.rb +68 -0
  183. data/examples/spec-app/spec/spec_helper.rb +17 -0
  184. data/examples/test-app/README.md +57 -0
  185. data/examples/test-app/app.rb +212 -0
  186. data/examples/test-app/docs/api/README.md +45 -0
  187. data/examples/test-app/docs/api/api/v1/customers/create.md +95 -0
  188. data/examples/test-app/docs/api/api/v1/customers/index.md +11 -0
  189. data/examples/test-app/docs/api/api/v1/customers/list.md +67 -0
  190. data/examples/test-app/docs/api/api/v1/customers/show.md +65 -0
  191. data/examples/test-app/docs/api/api/v1/tasks/complete.md +51 -0
  192. data/examples/test-app/docs/api/api/v1/tasks/create.md +90 -0
  193. data/examples/test-app/docs/api/api/v1/tasks/destroy.md +35 -0
  194. data/examples/test-app/docs/api/api/v1/tasks/index.md +14 -0
  195. data/examples/test-app/docs/api/api/v1/tasks/list.md +65 -0
  196. data/examples/test-app/docs/api/api/v1/tasks/show.md +65 -0
  197. data/examples/test-app/docs/api/api/v1/tasks/update.md +74 -0
  198. data/examples/test-app/docs/api/api/v1/users/create.md +95 -0
  199. data/examples/test-app/docs/api/api/v1/users/index.md +11 -0
  200. data/examples/test-app/docs/api/api/v1/users/list.md +67 -0
  201. data/examples/test-app/docs/api/api/v1/users/show.md +65 -0
  202. data/examples/test-app/docs/api/curl/api/v1/customers/create.sh +17 -0
  203. data/examples/test-app/docs/api/curl/api/v1/customers/list.sh +9 -0
  204. data/examples/test-app/docs/api/curl/api/v1/customers/show.sh +9 -0
  205. data/examples/test-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
  206. data/examples/test-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
  207. data/examples/test-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
  208. data/examples/test-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
  209. data/examples/test-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
  210. data/examples/test-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
  211. data/examples/test-app/docs/api/curl/api/v1/users/create.sh +17 -0
  212. data/examples/test-app/docs/api/curl/api/v1/users/list.sh +9 -0
  213. data/examples/test-app/docs/api/curl/api/v1/users/show.sh +9 -0
  214. data/examples/test-app/docs/api/dataset.json +2402 -0
  215. data/examples/test-app/docs/api/postman/collection.json +1290 -0
  216. data/examples/test-app/generate.rb +14 -0
  217. data/examples/test-app/reqcord.yml +28 -0
  218. data/examples/test-app/test/integration/customers_test.rb +72 -0
  219. data/examples/test-app/test/integration/tasks_test.rb +80 -0
  220. data/examples/test-app/test/integration/users_test.rb +72 -0
  221. data/lib/reqcord/capture/collector.rb +31 -0
  222. data/lib/reqcord/capture/integration_patch.rb +209 -0
  223. data/lib/reqcord/capture/minitest_context.rb +34 -0
  224. data/lib/reqcord/capture/rspec_context.rb +42 -0
  225. data/lib/reqcord/capture/test_context.rb +25 -0
  226. data/lib/reqcord/capture.rb +19 -0
  227. data/lib/reqcord/configuration.rb +198 -0
  228. data/lib/reqcord/dataset.rb +176 -0
  229. data/lib/reqcord/endpoint.rb +263 -0
  230. data/lib/reqcord/errors.rb +9 -0
  231. data/lib/reqcord/exporters/curl.rb +68 -0
  232. data/lib/reqcord/exporters/markdown.rb +295 -0
  233. data/lib/reqcord/exporters/postman.rb +206 -0
  234. data/lib/reqcord/exporters.rb +32 -0
  235. data/lib/reqcord/generator.rb +364 -0
  236. data/lib/reqcord/railtie.rb +51 -0
  237. data/lib/reqcord/renderers/curl.rb +56 -0
  238. data/lib/reqcord/renderers/payload.rb +69 -0
  239. data/lib/reqcord/request_example.rb +104 -0
  240. data/lib/reqcord/response_example.rb +72 -0
  241. data/lib/reqcord/route_collector.rb +242 -0
  242. data/lib/reqcord/sanitizers/sanitizer.rb +140 -0
  243. data/lib/reqcord/schema.rb +187 -0
  244. data/lib/reqcord/support.rb +58 -0
  245. data/lib/reqcord/version.rb +5 -0
  246. data/lib/reqcord.rb +78 -0
  247. data/lib/tasks/reqcord.rake +99 -0
  248. data/reqcord.gemspec +46 -0
  249. metadata +317 -3
@@ -0,0 +1,319 @@
1
+ # `reqcord.yml` reference
2
+
3
+ Reqcord reads one file, `reqcord.yml`, from the application root (`Rails.root`).
4
+ `bin/rails reqcord:init` writes a starting point; every key is optional.
5
+
6
+ ```yaml
7
+ version: 1
8
+
9
+ test:
10
+ framework: minitest
11
+ paths:
12
+ - test/integration
13
+ - test/api
14
+
15
+ routes:
16
+ prefix: /api
17
+
18
+ output:
19
+ directory: docs/api
20
+ include_uncovered: false
21
+
22
+ exporters:
23
+ - curl
24
+ - markdown
25
+ - postman
26
+
27
+ variables:
28
+ base_url: http://localhost:3000
29
+
30
+ sanitize:
31
+ headers:
32
+ Authorization: "Bearer {{token}}"
33
+ X-Api-Key: "{{api_key}}"
34
+ body:
35
+ password: "{{password}}"
36
+ ```
37
+
38
+ ## How values are resolved
39
+
40
+ ```text
41
+ environment variable > reqcord.yml > Reqcord default
42
+ ```
43
+
44
+ The file is **deep-merged** over the defaults: setting `sanitize.headers.X-Account-Id`
45
+ keeps the default `Authorization` and `X-Api-Key` entries. Lists are replaced,
46
+ not merged: an `exporters:` list is used exactly as written.
47
+
48
+ The file must be a YAML mapping; anything else raises `Reqcord::ConfigurationError`
49
+ before any test runs. YAML aliases are disabled.
50
+
51
+ | Variable | Overrides |
52
+ | --- | --- |
53
+ | `REQCORD_TEST_FRAMEWORK` | `test.framework` |
54
+ | `REQCORD_TEST_COMMAND` | `test.command` (and therefore `test.paths`) |
55
+ | `REQCORD_OUTPUT` | `output.directory` |
56
+ | `REQCORD_BASE_URL` | `variables.base_url` |
57
+ | `RESOURCE`, `VERSION` | run-time filters, see [Filtering a run](#filtering-a-run) |
58
+
59
+ `REQCORD_CAPTURE` and `REQCORD_CAPTURE_FILE` are set **by** Reqcord on the test
60
+ process it starts; do not set them yourself. Without them an ordinary
61
+ `bin/rails test` patches nothing and writes nothing.
62
+
63
+ ---
64
+
65
+ ## `version`
66
+
67
+ ```yaml
68
+ version: 1
69
+ ```
70
+
71
+ The configuration format version. Informational today; keep it at `1`.
72
+
73
+ ---
74
+
75
+ ## `test`
76
+
77
+ Where the requests come from. `reqcord:generate` runs your suite in a
78
+ subprocess with capture enabled; every request the integration tests make
79
+ is captured, so point this at the tests that exercise the API.
80
+
81
+ ### `test.framework`
82
+
83
+ `minitest` (default) or `rspec`. Decides two things: how the suite is run
84
+ when only `paths` is given, and how captured examples are named — from the
85
+ Minitest test method (`test "creates customer"`) or the RSpec example
86
+ (`it "creates customer"`). Any other value is rejected before the run.
87
+
88
+ ### `test.paths`
89
+
90
+ Directories, files or globs. Reqcord builds the runner:
91
+
92
+ | Framework | Project has `bin/rails` | Command |
93
+ | --- | --- | --- |
94
+ | minitest | yes | `bin/rails test <paths…>` |
95
+ | minitest | no | `ruby -Itest -e '<require each file>' <every *_test.rb beneath the paths>` |
96
+ | rspec | — | `rspec <paths…>` |
97
+
98
+ A directory means every `*_test.rb` (or `*_spec.rb`) beneath it; a glob such as
99
+ `test/api/*_test.rb` is expanded by Reqcord.
100
+
101
+ ```yaml
102
+ test:
103
+ framework: minitest
104
+ paths:
105
+ - test/api
106
+ - test/controllers/api
107
+ - test/integration
108
+ ```
109
+
110
+ ### `test.command`
111
+
112
+ Spell the command out yourself. It wins over `paths`.
113
+
114
+ ```yaml
115
+ test:
116
+ command: bin/rails test test/api test/integration/orders_test.rb
117
+ ```
118
+
119
+ The command is split into words and executed **without a shell**: no pipes,
120
+ `&&`, environment assignments or quoting tricks. Globs in the arguments are
121
+ expanded by Reqcord (`test/api/*_test.rb` works). An empty string counts as
122
+ absent.
123
+
124
+ With neither `paths` nor `command`, Reqcord runs `bin/rails test`.
125
+
126
+ Whatever runs must exit successfully. A failing suite aborts the run with
127
+ `Reqcord::GenerationError` and nothing is written — documentation is only
128
+ generated from a green suite.
129
+
130
+ ---
131
+
132
+ ## `routes`
133
+
134
+ The documented surface is the application's route table (including mounted
135
+ engines), not the captured traffic: every matching route becomes an endpoint,
136
+ and captures are attached to it.
137
+
138
+ ### `routes.prefix`
139
+
140
+ Default `/api`. Only routes whose path starts with the prefix are documented.
141
+ For a mounted engine the prefix is matched against the full path
142
+ (`/api/billing/invoices`). Set it to an empty string to document every route.
143
+
144
+ ```yaml
145
+ routes:
146
+ prefix: /api/v2
147
+ ```
148
+
149
+ Rails' own routes (`rails/…`, Active Storage, Action Mailbox, Turbo) are always
150
+ left out. `redirect(...)` routes and plain Rack mounts cannot be documented from
151
+ a test; they are counted as *skipped* in the report rather than dropped.
152
+
153
+ ### Filtering a run
154
+
155
+ Filters are given per run, not in the file:
156
+
157
+ ```bash
158
+ bin/rails reqcord:generate RESOURCE=customers
159
+ bin/rails reqcord:generate RESOURCE=customers,cart
160
+ bin/rails reqcord:generate VERSION=v2
161
+ bin/rails reqcord:generate RESOURCE=orders VERSION=v1
162
+ ```
163
+
164
+ `RESOURCE` matches the controller's last segment (`customers`), its singular
165
+ (`cart` for `CartsController`) or the full controller path
166
+ (`api/v2/customers`). `VERSION` matches a `v<number>` segment in the controller
167
+ path or the route path.
168
+
169
+ ---
170
+
171
+ ## `output`
172
+
173
+ ### `output.directory`
174
+
175
+ Default `docs/api`, relative to the application root (an absolute path is used
176
+ as is). Override per run with `REQCORD_OUTPUT`. A run writes:
177
+
178
+ ```text
179
+ <directory>/
180
+ ├── dataset.json the canonical dataset every exporter reads
181
+ ├── README.md index of resources and endpoints (markdown)
182
+ ├── api/v1/customers/ one directory per controller path (markdown)
183
+ │ ├── index.md
184
+ │ └── create.md
185
+ ├── curl/api/v1/customers/create.sh (curl)
186
+ └── postman/collection.json (postman)
187
+ ```
188
+
189
+ Directories follow the controller path, so `admin/customers` and
190
+ `api/v1/customers` never collide.
191
+
192
+ ### `output.include_uncovered`
193
+
194
+ Default `false`. A route no test reached with a `2xx` response is listed in
195
+ the index under *No Successful Request Captured* and in `dataset.json` under
196
+ `uncovered_routes`, but gets no page of its own. Set `true` to write those
197
+ pages as well, each carrying a note that nothing was captured.
198
+
199
+ ---
200
+
201
+ ## `exporters`
202
+
203
+ Which outputs to write. Default: all three.
204
+
205
+ | Name | Writes |
206
+ | --- | --- |
207
+ | `markdown` | `README.md` plus one page per endpoint: headers, typed parameter tables, example request, cURL, one example and field table per response status |
208
+ | `curl` | one runnable `.sh` per endpoint under `curl/`, built from the successful captured request |
209
+ | `postman` | `postman/collection.json`, a Postman Collection v2.1 — folders per controller namespace, one request per endpoint, every captured status saved as an example, placeholders as collection variables, bearer auth at collection level. Hoppscotch imports the same file |
210
+
211
+ `dataset.json` is always written. An unknown name raises
212
+ `Reqcord::ConfigurationError` before any test runs.
213
+
214
+ ---
215
+
216
+ ## `variables`
217
+
218
+ ### `variables.base_url`
219
+
220
+ Default `http://localhost:3000`. The host in every generated cURL and the value
221
+ of the Postman `base_url` collection variable. A trailing slash is dropped.
222
+ Override per run with `REQCORD_BASE_URL`.
223
+
224
+ Other keys under `variables` are read but not used by the built-in exporters.
225
+
226
+ ---
227
+
228
+ ## `sanitize`
229
+
230
+ Captured tests carry real credentials. Sanitization runs on every exchange
231
+ before anything reaches the dataset, so generated files never contain them.
232
+
233
+ ### `sanitize.headers`
234
+
235
+ Header name → replacement, matched case-insensitively, replaced **verbatim**:
236
+
237
+ ```yaml
238
+ sanitize:
239
+ headers:
240
+ Authorization: "Bearer {{token}}"
241
+ X-Api-Key: "{{api_key}}"
242
+ X-Account-Id: "{{account_id}}"
243
+ ```
244
+
245
+ Defaults: `Authorization: "Bearer {{token}}"` and `X-Api-Key: "{{api_key}}"`.
246
+
247
+ Some headers are redacted **whether configured or not**: `Authorization`,
248
+ `Proxy-Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, `X-Auth-Token`,
249
+ `X-Csrf-Token`. Without a configured replacement the value becomes a
250
+ placeholder named after the header (`{{cookie}}`, `{{x_auth_token}}`), and a
251
+ `Bearer`, `Token` or `Basic` scheme is kept so the cURL stays runnable
252
+ (`Basic {{authorization}}`).
253
+
254
+ Two more things happen to headers, on requests and responses alike:
255
+
256
+ * transport noise is dropped: `Host`, `User-Agent`, `Connection`, `Version`,
257
+ `Remote-Addr`, `Accept-Encoding`, `Cache-Control`, `Content-Length`, `Date`,
258
+ `ETag`, `Server-Timing`, `Transfer-Encoding`, `Vary`, `X-Request-Id`,
259
+ `X-Runtime`, `Referrer-Policy` and the `X-*-Options` security headers;
260
+ * headers with an empty value are dropped.
261
+
262
+ ### `sanitize.body`
263
+
264
+ Body key → replacement, matched case-insensitively **at any depth**, in
265
+ request and response bodies alike. The whole value under the key is replaced,
266
+ whatever its type.
267
+
268
+ ```yaml
269
+ sanitize:
270
+ body:
271
+ password: "{{password}}"
272
+ payment_url: "{{payment_url}}"
273
+ ```
274
+
275
+ Defaults: `password`, `password_confirmation`, `token`, `access_token`,
276
+ `refresh_token`, `api_key`, `secret`, `client_secret` (→ `{{password}}`,
277
+ `{{token}}`, `{{api_key}}`, `{{secret}}`).
278
+
279
+ A sanitized value is what the documentation shows, so it also shapes the
280
+ parameter tables: two logins whose passwords both became `{{password}}` are two
281
+ examples of the same request, and the one that succeeded is the one used for
282
+ the cURL.
283
+
284
+ ### Placeholders
285
+
286
+ Every `{{name}}` you write is kept as-is in Markdown and cURL, listed in the
287
+ index under *Placeholders*, and declared as a variable in the Postman
288
+ collection — Postman's variable syntax is the same, so the collection runs as
289
+ soon as `base_url` and `token` are filled in.
290
+
291
+ ---
292
+
293
+ ## Rake tasks
294
+
295
+ | Task | Does |
296
+ | --- | --- |
297
+ | `bin/rails reqcord:init` | writes `reqcord.yml` (never overwrites) and creates `docs/api/` |
298
+ | `bin/rails reqcord:generate` | collects routes, runs the suite with capture, writes every exporter, prints the report |
299
+ | `bin/rails reqcord:routes` | lists the routes the current `prefix` (and `RESOURCE` / `VERSION`) would document |
300
+
301
+ Every `generate` run ends with a reconciliation of the whole route table:
302
+
303
+ ```text
304
+ [reqcord] captured 92 request(s), 87 matched a documented route
305
+ [reqcord] captured a successful 2xx request for 14 of 16 endpoint(s)
306
+ [reqcord] routes: 16 = 14 documented + 2 uncovered + 0 skipped
307
+ ```
308
+
309
+ Each route is in exactly one bucket; a captured request that matched no
310
+ documented route is listed, never dropped silently.
311
+
312
+ ## When the output looks thin
313
+
314
+ | Symptom | Usual cause |
315
+ | --- | --- |
316
+ | `no request was captured` | the gem is not in the `:test` group of the Gemfile, or `test.paths` / `test.command` runs no integration tests |
317
+ | many requests captured, few matched | `routes.prefix` does not cover them — the unmatched paths are printed |
318
+ | routes documented but few covered | the tests that exercise them are not in `test.paths` (a `2xx` from a test is what makes an endpoint documented) |
319
+ | `Test suite failed while generating` | the suite is red; fix the tests, documentation is only generated from a passing run |
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,21 @@
1
+ # complex-spec-app (RSpec)
2
+
3
+ The store API of [complex-test-app](../complex-test-app), documented from
4
+ RSpec request specs instead of Minitest. The application file is identical;
5
+ only the specs differ, and the generated dataset lists the same 22 endpoints.
6
+
7
+ ```text
8
+ app.rb the application (same as complex-test-app)
9
+ spec/requests/*_spec.rb ordinary request specs
10
+ spec/spec_helper.rb request helpers for plain rspec-core (see spec-app)
11
+ reqcord.yml `test.framework: rspec`, `paths: [spec/requests]`
12
+ docs/api/ generated Markdown, cURL scripts, Postman collection
13
+ ```
14
+
15
+ ## Regenerate
16
+
17
+ ```bash
18
+ ruby generate.rb
19
+ ```
20
+
21
+ See the endpoint tour in the [complex-test-app README](../complex-test-app/README.md).
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The store API of complex-test-app, byte for byte, documented from RSpec
4
+ # request specs instead of Minitest. See ../complex-test-app/app.rb for the
5
+ # tour of what each endpoint shows.
6
+ ENV["RAILS_ENV"] ||= "test"
7
+
8
+ # Only needed because this example lives inside the Reqcord repository.
9
+ # In your own application, `gem "reqcord"` in the Gemfile is enough.
10
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
11
+
12
+ require "rails"
13
+ require "action_controller/railtie"
14
+ require "reqcord"
15
+
16
+ class ComplexApp < Rails::Application
17
+ config.root = __dir__
18
+ config.eager_load = false
19
+ config.logger = Logger.new(IO::NULL)
20
+ config.secret_key_base = "a" * 64
21
+ config.hosts.clear
22
+ end
23
+
24
+ Rails.application.initialize!
25
+
26
+ module Store
27
+ PRODUCTS = [
28
+ { id: 1, sku: "TEA-001", name: "Earl Grey", category: "tea", price_cents: 1200, cost_cents: 500, tags: %w[black bergamot] },
29
+ { id: 2, sku: "TEA-002", name: "Sencha", category: "tea", price_cents: 1500, cost_cents: 700, tags: %w[green] },
30
+ { id: 3, sku: "MUG-001", name: "Stoneware Mug", category: "mugs", price_cents: 2400, cost_cents: 900, tags: %w[ceramic] },
31
+ { id: 4, sku: "MUG-002", name: "Travel Mug", category: "mugs", price_cents: 3200, cost_cents: 1400, tags: %w[steel insulated] }
32
+ ].freeze
33
+
34
+ PUBLIC_PRODUCT = %i[id sku name category price_cents tags].freeze
35
+
36
+ ORDERS = [
37
+ { id: 1, user_id: 1, status: "pending", currency: "USD", total_cents: 2700 },
38
+ { id: 2, user_id: 1, status: "shipped", currency: "USD", total_cents: 3200 },
39
+ { id: 3, user_id: 2, status: "pending", currency: "EUR", total_cents: 1500 }
40
+ ].freeze
41
+
42
+ NOTES = [
43
+ { id: 1, order_id: 1, body: "Leave at the door", author: "ada" },
44
+ { id: 2, order_id: 1, body: "Gift wrap please", author: "ada" }
45
+ ].freeze
46
+
47
+ PROFILE = { id: 1, name: "Ada Lovelace", email: "ada@example.com", locale: "en" }.freeze
48
+ LOCALES = %w[en tr].freeze
49
+
50
+ CART_ITEMS = [
51
+ { sku: "TEA-001", name: "Earl Grey", quantity: 2, unit_price_cents: 1200 },
52
+ { sku: "MUG-001", name: "Stoneware Mug", quantity: 1, unit_price_cents: 2400 }
53
+ ].freeze
54
+ PAYMENT_METHODS = %w[card bank_transfer].freeze
55
+ end
56
+
57
+ module Api
58
+ module V1
59
+ class BaseController < ActionController::API
60
+ private
61
+
62
+ def authenticate!
63
+ return if request.headers["Authorization"].present?
64
+
65
+ render json: { error: "Unauthorized" }, status: :unauthorized
66
+ end
67
+
68
+ def not_found
69
+ render json: { error: "Not Found" }, status: :not_found
70
+ end
71
+ end
72
+
73
+ class HomeController < BaseController
74
+ def index
75
+ render json: { name: "Store API", version: "v1", links: { products: "/api/v1/products", orders: "/api/v1/orders" } }
76
+ end
77
+ end
78
+
79
+ class AuthController < BaseController
80
+ def login
81
+ if params[:email] == "ada@example.com" && params[:password] == "correct-horse-battery"
82
+ render json: { token: "tok_live_9f8e7d6c", token_type: "Bearer", expires_in: 3600 }
83
+ else
84
+ render json: { error: "Invalid email or password" }, status: :unauthorized
85
+ end
86
+ end
87
+ end
88
+
89
+ class ProfilesController < BaseController
90
+ before_action :authenticate!
91
+
92
+ def show
93
+ render json: Store::PROFILE
94
+ end
95
+
96
+ def update
97
+ attributes = params.require(:profile).permit(:name, :locale).to_h.symbolize_keys
98
+
99
+ if attributes.key?(:locale) && !Store::LOCALES.include?(attributes[:locale])
100
+ return render(json: { errors: { locale: ["is not included in the list"] } }, status: :unprocessable_entity)
101
+ end
102
+
103
+ render json: Store::PROFILE.merge(attributes)
104
+ end
105
+ end
106
+
107
+ class ProductsController < BaseController
108
+ def index
109
+ products = Store::PRODUCTS
110
+ category = params.dig(:filter, :category)
111
+ products = products.select { |product| product[:category] == category } if category.present?
112
+ products = products.sort_by { |product| product[:price_cents] } if params[:sort] == "price_asc"
113
+ products = products.sort_by { |product| -product[:price_cents] } if params[:sort] == "price_desc"
114
+
115
+ page = params.fetch(:page, 1).to_i
116
+ per_page = params.fetch(:per_page, 2).to_i
117
+ slice = products.each_slice(per_page).to_a[page - 1] || []
118
+
119
+ render json: {
120
+ data: slice.map { |product| product.slice(*Store::PUBLIC_PRODUCT) },
121
+ meta: { page: page, per_page: per_page, total: products.size }
122
+ }
123
+ end
124
+
125
+ def show
126
+ product = Store::PRODUCTS.find { |candidate| candidate[:id] == params[:id].to_i }
127
+
128
+ return not_found unless product
129
+
130
+ render json: product.slice(*Store::PUBLIC_PRODUCT)
131
+ end
132
+
133
+ def search
134
+ query = params[:q].to_s.strip
135
+
136
+ return render(json: { error: "q is required" }, status: :bad_request) if query.empty?
137
+
138
+ matches = Store::PRODUCTS.select { |product| product[:name].downcase.include?(query.downcase) }
139
+
140
+ render json: { query: query, results: matches.map { |product| product.slice(*Store::PUBLIC_PRODUCT) } }
141
+ end
142
+ end
143
+
144
+ class OrdersController < BaseController
145
+ before_action :authenticate!
146
+ before_action :find_order, only: %i[show cancel destroy]
147
+
148
+ def index
149
+ orders = Store::ORDERS.select { |order| order[:user_id] == 1 }
150
+ orders = orders.select { |order| order[:status] == params[:status] } if params[:status].present?
151
+
152
+ render json: orders
153
+ end
154
+
155
+ def show
156
+ return render(json: { error: "Forbidden" }, status: :forbidden) unless @order[:user_id] == 1
157
+
158
+ render json: @order
159
+ end
160
+
161
+ def create
162
+ attributes = params.require(:order)
163
+ .permit(line_items: %i[sku quantity], shipping_address: %i[line1 city country])
164
+ .to_h.deep_symbolize_keys
165
+ items = Array(attributes[:line_items])
166
+
167
+ return render(json: { errors: { line_items: ["can't be blank"] } }, status: :unprocessable_entity) if items.empty?
168
+
169
+ lines = items.map do |item|
170
+ product = Store::PRODUCTS.find { |candidate| candidate[:sku] == item[:sku] }
171
+
172
+ return render(json: { errors: { sku: ["#{item[:sku]} is not a known product"] } }, status: :unprocessable_entity) unless product
173
+
174
+ quantity = item[:quantity].to_i
175
+ {
176
+ sku: product[:sku], name: product[:name], quantity: quantity,
177
+ unit_price_cents: product[:price_cents], subtotal_cents: product[:price_cents] * quantity
178
+ }
179
+ end
180
+
181
+ render json: {
182
+ id: 4, status: "pending", currency: "USD",
183
+ total_cents: lines.sum { |line| line[:subtotal_cents] },
184
+ line_items: lines,
185
+ shipping_address: attributes[:shipping_address]
186
+ }, status: :created
187
+ end
188
+
189
+ def cancel
190
+ if @order[:status] == "shipped"
191
+ return render(json: { error: "Shipped orders cannot be cancelled" }, status: :conflict)
192
+ end
193
+
194
+ render json: @order.merge(status: "cancelled")
195
+ end
196
+
197
+ def destroy
198
+ head :no_content
199
+ end
200
+
201
+ private
202
+
203
+ def find_order
204
+ @order = Store::ORDERS.find { |order| order[:id] == params[:id].to_i }
205
+
206
+ not_found unless @order
207
+ end
208
+ end
209
+
210
+ class NotesController < BaseController
211
+ before_action :authenticate!
212
+ before_action :find_order
213
+
214
+ def index
215
+ render json: Store::NOTES.select { |note| note[:order_id] == @order[:id] }
216
+ end
217
+
218
+ def create
219
+ body = params.dig(:note, :body).to_s
220
+
221
+ return render(json: { errors: { body: ["can't be blank"] } }, status: :unprocessable_entity) if body.empty?
222
+
223
+ render json: { id: 3, order_id: @order[:id], body: body, author: "ada" }, status: :created
224
+ end
225
+
226
+ private
227
+
228
+ def find_order
229
+ @order = Store::ORDERS.find { |order| order[:id] == params[:order_id].to_i }
230
+
231
+ not_found unless @order
232
+ end
233
+ end
234
+
235
+ class CartsController < BaseController
236
+ before_action :authenticate!
237
+
238
+ def show
239
+ render json: cart
240
+ end
241
+
242
+ def checkout
243
+ method = params[:payment_method].to_s
244
+
245
+ unless Store::PAYMENT_METHODS.include?(method)
246
+ return render(json: { errors: { payment_method: ["is not included in the list"] } }, status: :unprocessable_entity)
247
+ end
248
+
249
+ render json: { id: 5, status: "pending", payment_method: method, total_cents: cart[:total_cents], line_items: cart[:items] },
250
+ status: :created
251
+ end
252
+
253
+ private
254
+
255
+ def cart
256
+ { items: Store::CART_ITEMS, total_cents: Store::CART_ITEMS.sum { |item| item[:quantity] * item[:unit_price_cents] } }
257
+ end
258
+ end
259
+
260
+ class CartItemsController < BaseController
261
+ before_action :authenticate!
262
+
263
+ def create
264
+ attributes = params.require(:item).permit(:sku, :quantity).to_h.symbolize_keys
265
+ product = Store::PRODUCTS.find { |candidate| candidate[:sku] == attributes[:sku] }
266
+
267
+ return render(json: { errors: { sku: ["#{attributes[:sku]} is not a known product"] } }, status: :unprocessable_entity) unless product
268
+
269
+ render json: { sku: product[:sku], name: product[:name], quantity: attributes[:quantity].to_i, unit_price_cents: product[:price_cents] },
270
+ status: :created
271
+ end
272
+
273
+ def destroy
274
+ return not_found unless Store::CART_ITEMS.any? { |item| item[:sku] == params[:sku] }
275
+
276
+ head :no_content
277
+ end
278
+ end
279
+
280
+ module Admin
281
+ class ProductsController < BaseController
282
+ before_action :require_api_key
283
+
284
+ def index
285
+ render json: Store::PRODUCTS
286
+ end
287
+
288
+ def create
289
+ attributes = params.require(:product).permit(:sku, :name, :category, :price_cents).to_h.symbolize_keys
290
+
291
+ if attributes[:sku].to_s.empty? || attributes[:name].to_s.empty?
292
+ return render(json: { errors: { sku: ["can't be blank"] } }, status: :unprocessable_entity)
293
+ end
294
+
295
+ if Store::PRODUCTS.any? { |product| product[:sku] == attributes[:sku] }
296
+ return render(json: { error: "sku #{attributes[:sku]} already exists" }, status: :conflict)
297
+ end
298
+
299
+ render json: { id: 5, cost_cents: 0, tags: [], **attributes }, status: :created
300
+ end
301
+
302
+ def destroy
303
+ return not_found unless Store::PRODUCTS.any? { |product| product[:id] == params[:id].to_i }
304
+
305
+ head :no_content
306
+ end
307
+
308
+ private
309
+
310
+ def require_api_key
311
+ return if request.headers["X-Api-Key"].present?
312
+
313
+ render json: { error: "API key required" }, status: :unauthorized
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ module V2
320
+ class ProductsController < ActionController::API
321
+ def index
322
+ start = params[:cursor].present? ? 2 : 0
323
+ items = Store::PRODUCTS[start, 2].map { |product| product.slice(*Store::PUBLIC_PRODUCT).merge(price: format("%.2f", product[:price_cents] / 100.0)) }
324
+
325
+ render json: { items: items, next_cursor: start.zero? ? "eyJpZCI6Mn0" : nil }
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ Rails.application.routes.draw do
332
+ namespace :api do
333
+ namespace :v1 do
334
+ root to: "home#index"
335
+
336
+ post "auth/login", to: "auth#login"
337
+
338
+ resource :profile, only: %i[show update]
339
+
340
+ resources :products, only: %i[index show] do
341
+ get :search, on: :collection
342
+ end
343
+
344
+ resources :orders, only: %i[index show create destroy] do
345
+ post :cancel, on: :member
346
+ resources :notes, only: %i[index create]
347
+ end
348
+
349
+ resource :cart, only: :show do
350
+ post :checkout
351
+ resources :items, only: %i[create destroy], controller: "cart_items", param: :sku
352
+ end
353
+
354
+ namespace :admin do
355
+ resources :products, only: %i[index create destroy]
356
+ end
357
+ end
358
+
359
+ namespace :v2 do
360
+ resources :products, only: :index
361
+ end
362
+ end
363
+ end