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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/CHANGELOG.md +77 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +171 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -95
- data/Rakefile +13 -0
- data/docs/configuration.md +319 -0
- data/examples/complex-spec-app/.rspec +1 -0
- data/examples/complex-spec-app/README.md +21 -0
- data/examples/complex-spec-app/app.rb +363 -0
- data/examples/complex-spec-app/docs/api/README.md +104 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/create.md +117 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/destroy.md +51 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/index.md +11 -0
- data/examples/complex-spec-app/docs/api/api/v1/admin/products/list.md +106 -0
- data/examples/complex-spec-app/docs/api/api/v1/auth/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v1/auth/login.md +76 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/create.md +91 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/destroy.md +51 -0
- data/examples/complex-spec-app/docs/api/api/v1/cart-items/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/checkout.md +102 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/carts/show.md +73 -0
- data/examples/complex-spec-app/docs/api/api/v1/home/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v1/home/list.md +47 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/create.md +94 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/notes/list.md +61 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/cancel.md +69 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/create.md +150 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/destroy.md +37 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/index.md +13 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/list.md +78 -0
- data/examples/complex-spec-app/docs/api/api/v1/orders/show.md +83 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/index.md +11 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/list.md +83 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/search.md +88 -0
- data/examples/complex-spec-app/docs/api/api/v1/products/show.md +71 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/index.md +10 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/show.md +61 -0
- data/examples/complex-spec-app/docs/api/api/v1/profiles/update.md +91 -0
- data/examples/complex-spec-app/docs/api/api/v2/products/index.md +9 -0
- data/examples/complex-spec-app/docs/api/api/v2/products/list.md +77 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/auth/login.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/carts/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/home/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/notes/create.sh +15 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/notes/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/create.sh +29 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/list.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/orders/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/search.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/products/show.sh +8 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
- data/examples/complex-spec-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
- data/examples/complex-spec-app/docs/api/curl/api/v2/products/list.sh +8 -0
- data/examples/complex-spec-app/docs/api/dataset.json +5116 -0
- data/examples/complex-spec-app/docs/api/postman/collection.json +2519 -0
- data/examples/complex-spec-app/generate.rb +14 -0
- data/examples/complex-spec-app/reqcord.yml +28 -0
- data/examples/complex-spec-app/spec/requests/admin_spec.rb +67 -0
- data/examples/complex-spec-app/spec/requests/cart_spec.rb +61 -0
- data/examples/complex-spec-app/spec/requests/home_and_auth_spec.rb +49 -0
- data/examples/complex-spec-app/spec/requests/orders_spec.rb +124 -0
- data/examples/complex-spec-app/spec/requests/products_spec.rb +65 -0
- data/examples/complex-spec-app/spec/spec_helper.rb +17 -0
- data/examples/complex-test-app/README.md +40 -0
- data/examples/complex-test-app/app.rb +384 -0
- data/examples/complex-test-app/docs/api/README.md +104 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/create.md +117 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/destroy.md +51 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/index.md +11 -0
- data/examples/complex-test-app/docs/api/api/v1/admin/products/list.md +106 -0
- data/examples/complex-test-app/docs/api/api/v1/auth/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v1/auth/login.md +76 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/create.md +91 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/destroy.md +51 -0
- data/examples/complex-test-app/docs/api/api/v1/cart-items/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/checkout.md +102 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/carts/show.md +73 -0
- data/examples/complex-test-app/docs/api/api/v1/home/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v1/home/list.md +47 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/create.md +94 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/notes/list.md +61 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/cancel.md +69 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/create.md +150 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/destroy.md +37 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/index.md +13 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/list.md +71 -0
- data/examples/complex-test-app/docs/api/api/v1/orders/show.md +83 -0
- data/examples/complex-test-app/docs/api/api/v1/products/index.md +11 -0
- data/examples/complex-test-app/docs/api/api/v1/products/list.md +83 -0
- data/examples/complex-test-app/docs/api/api/v1/products/search.md +88 -0
- data/examples/complex-test-app/docs/api/api/v1/products/show.md +71 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/index.md +10 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/show.md +61 -0
- data/examples/complex-test-app/docs/api/api/v1/profiles/update.md +91 -0
- data/examples/complex-test-app/docs/api/api/v2/products/index.md +9 -0
- data/examples/complex-test-app/docs/api/api/v2/products/list.md +77 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/create.sh +18 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/admin/products/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/auth/login.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/create.sh +16 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/cart-items/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/carts/checkout.sh +13 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/carts/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/home/list.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/notes/create.sh +15 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/notes/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/cancel.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/create.sh +29 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/destroy.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/list.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/orders/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/list.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/search.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/products/show.sh +8 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/profiles/show.sh +9 -0
- data/examples/complex-test-app/docs/api/curl/api/v1/profiles/update.sh +16 -0
- data/examples/complex-test-app/docs/api/curl/api/v2/products/list.sh +8 -0
- data/examples/complex-test-app/docs/api/dataset.json +5109 -0
- data/examples/complex-test-app/docs/api/postman/collection.json +2568 -0
- data/examples/complex-test-app/generate.rb +14 -0
- data/examples/complex-test-app/reqcord.yml +28 -0
- data/examples/complex-test-app/test/integration/admin_test.rb +70 -0
- data/examples/complex-test-app/test/integration/cart_test.rb +64 -0
- data/examples/complex-test-app/test/integration/home_and_auth_test.rb +53 -0
- data/examples/complex-test-app/test/integration/orders_test.rb +127 -0
- data/examples/complex-test-app/test/integration/products_test.rb +66 -0
- data/examples/reqcord.yml +58 -0
- data/examples/spec-app/.rspec +1 -0
- data/examples/spec-app/README.md +36 -0
- data/examples/spec-app/app.rb +209 -0
- data/examples/spec-app/docs/api/README.md +45 -0
- data/examples/spec-app/docs/api/api/v1/customers/create.md +95 -0
- data/examples/spec-app/docs/api/api/v1/customers/index.md +11 -0
- data/examples/spec-app/docs/api/api/v1/customers/list.md +67 -0
- data/examples/spec-app/docs/api/api/v1/customers/show.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/complete.md +51 -0
- data/examples/spec-app/docs/api/api/v1/tasks/create.md +90 -0
- data/examples/spec-app/docs/api/api/v1/tasks/destroy.md +35 -0
- data/examples/spec-app/docs/api/api/v1/tasks/index.md +14 -0
- data/examples/spec-app/docs/api/api/v1/tasks/list.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/show.md +65 -0
- data/examples/spec-app/docs/api/api/v1/tasks/update.md +74 -0
- data/examples/spec-app/docs/api/api/v1/users/create.md +95 -0
- data/examples/spec-app/docs/api/api/v1/users/index.md +11 -0
- data/examples/spec-app/docs/api/api/v1/users/list.md +67 -0
- data/examples/spec-app/docs/api/api/v1/users/show.md +65 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/create.sh +17 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/list.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/customers/show.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
- data/examples/spec-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/create.sh +17 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/list.sh +9 -0
- data/examples/spec-app/docs/api/curl/api/v1/users/show.sh +9 -0
- data/examples/spec-app/docs/api/dataset.json +2402 -0
- data/examples/spec-app/docs/api/postman/collection.json +1290 -0
- data/examples/spec-app/generate.rb +14 -0
- data/examples/spec-app/reqcord.yml +27 -0
- data/examples/spec-app/spec/requests/customers_spec.rb +71 -0
- data/examples/spec-app/spec/requests/tasks_spec.rb +78 -0
- data/examples/spec-app/spec/requests/users_spec.rb +68 -0
- data/examples/spec-app/spec/spec_helper.rb +17 -0
- data/examples/test-app/README.md +57 -0
- data/examples/test-app/app.rb +212 -0
- data/examples/test-app/docs/api/README.md +45 -0
- data/examples/test-app/docs/api/api/v1/customers/create.md +95 -0
- data/examples/test-app/docs/api/api/v1/customers/index.md +11 -0
- data/examples/test-app/docs/api/api/v1/customers/list.md +67 -0
- data/examples/test-app/docs/api/api/v1/customers/show.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/complete.md +51 -0
- data/examples/test-app/docs/api/api/v1/tasks/create.md +90 -0
- data/examples/test-app/docs/api/api/v1/tasks/destroy.md +35 -0
- data/examples/test-app/docs/api/api/v1/tasks/index.md +14 -0
- data/examples/test-app/docs/api/api/v1/tasks/list.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/show.md +65 -0
- data/examples/test-app/docs/api/api/v1/tasks/update.md +74 -0
- data/examples/test-app/docs/api/api/v1/users/create.md +95 -0
- data/examples/test-app/docs/api/api/v1/users/index.md +11 -0
- data/examples/test-app/docs/api/api/v1/users/list.md +67 -0
- data/examples/test-app/docs/api/api/v1/users/show.md +65 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/create.sh +17 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/list.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/customers/show.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/complete.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/create.sh +15 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/destroy.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/list.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/show.sh +8 -0
- data/examples/test-app/docs/api/curl/api/v1/tasks/update.sh +14 -0
- data/examples/test-app/docs/api/curl/api/v1/users/create.sh +17 -0
- data/examples/test-app/docs/api/curl/api/v1/users/list.sh +9 -0
- data/examples/test-app/docs/api/curl/api/v1/users/show.sh +9 -0
- data/examples/test-app/docs/api/dataset.json +2402 -0
- data/examples/test-app/docs/api/postman/collection.json +1290 -0
- data/examples/test-app/generate.rb +14 -0
- data/examples/test-app/reqcord.yml +28 -0
- data/examples/test-app/test/integration/customers_test.rb +72 -0
- data/examples/test-app/test/integration/tasks_test.rb +80 -0
- data/examples/test-app/test/integration/users_test.rb +72 -0
- data/lib/reqcord/capture/collector.rb +31 -0
- data/lib/reqcord/capture/integration_patch.rb +209 -0
- data/lib/reqcord/capture/minitest_context.rb +34 -0
- data/lib/reqcord/capture/rspec_context.rb +42 -0
- data/lib/reqcord/capture/test_context.rb +25 -0
- data/lib/reqcord/capture.rb +19 -0
- data/lib/reqcord/configuration.rb +198 -0
- data/lib/reqcord/dataset.rb +176 -0
- data/lib/reqcord/endpoint.rb +263 -0
- data/lib/reqcord/errors.rb +9 -0
- data/lib/reqcord/exporters/curl.rb +68 -0
- data/lib/reqcord/exporters/markdown.rb +295 -0
- data/lib/reqcord/exporters/postman.rb +206 -0
- data/lib/reqcord/exporters.rb +32 -0
- data/lib/reqcord/generator.rb +364 -0
- data/lib/reqcord/railtie.rb +51 -0
- data/lib/reqcord/renderers/curl.rb +56 -0
- data/lib/reqcord/renderers/payload.rb +69 -0
- data/lib/reqcord/request_example.rb +104 -0
- data/lib/reqcord/response_example.rb +72 -0
- data/lib/reqcord/route_collector.rb +242 -0
- data/lib/reqcord/sanitizers/sanitizer.rb +140 -0
- data/lib/reqcord/schema.rb +187 -0
- data/lib/reqcord/support.rb +58 -0
- data/lib/reqcord/version.rb +5 -0
- data/lib/reqcord.rb +78 -0
- data/lib/tasks/reqcord.rake +99 -0
- data/reqcord.gemspec +46 -0
- 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
|