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,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULTS = {
|
|
6
|
+
"version" => 1,
|
|
7
|
+
|
|
8
|
+
# No default command: with neither `command` nor `paths` the generator
|
|
9
|
+
# falls back to `bin/rails test`, and `paths` alone must be able to win.
|
|
10
|
+
"test" => {
|
|
11
|
+
"framework" => "minitest"
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"routes" => {
|
|
15
|
+
"prefix" => "/api"
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
"output" => {
|
|
19
|
+
"directory" => "docs/api",
|
|
20
|
+
|
|
21
|
+
# A route no test exercised is reported in the index; writing a page
|
|
22
|
+
# with nothing on it only adds noise.
|
|
23
|
+
"include_uncovered" => false
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"exporters" => %w[curl markdown postman],
|
|
27
|
+
|
|
28
|
+
"variables" => {
|
|
29
|
+
"base_url" => "http://localhost:3000"
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"sanitize" => {
|
|
33
|
+
"headers" => {
|
|
34
|
+
"Authorization" => "Bearer {{token}}",
|
|
35
|
+
"X-Api-Key" => "{{api_key}}"
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"body" => {
|
|
39
|
+
"password" => "{{password}}",
|
|
40
|
+
"password_confirmation" => "{{password}}",
|
|
41
|
+
"token" => "{{token}}",
|
|
42
|
+
"access_token" => "{{token}}",
|
|
43
|
+
"refresh_token" => "{{token}}",
|
|
44
|
+
"api_key" => "{{api_key}}",
|
|
45
|
+
"secret" => "{{secret}}",
|
|
46
|
+
"client_secret" => "{{secret}}"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Headers that describe the transport rather than the API. A documented
|
|
52
|
+
# cURL that carries them is worse than one that does not: `Host` alone
|
|
53
|
+
# would send the reader's request to the wrong virtual host.
|
|
54
|
+
NOISY_HEADERS = %w[
|
|
55
|
+
host
|
|
56
|
+
user-agent
|
|
57
|
+
connection
|
|
58
|
+
version
|
|
59
|
+
remote-addr
|
|
60
|
+
accept-encoding
|
|
61
|
+
cache-control
|
|
62
|
+
content-length
|
|
63
|
+
date
|
|
64
|
+
etag
|
|
65
|
+
server-timing
|
|
66
|
+
transfer-encoding
|
|
67
|
+
vary
|
|
68
|
+
x-content-type-options
|
|
69
|
+
x-download-options
|
|
70
|
+
x-frame-options
|
|
71
|
+
x-permitted-cross-domain-policies
|
|
72
|
+
x-request-id
|
|
73
|
+
x-runtime
|
|
74
|
+
x-xss-protection
|
|
75
|
+
referrer-policy
|
|
76
|
+
].freeze
|
|
77
|
+
|
|
78
|
+
attr_reader :data, :root
|
|
79
|
+
|
|
80
|
+
def self.load(root:)
|
|
81
|
+
new(root: root).load
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def initialize(root:)
|
|
85
|
+
@root = Pathname(root)
|
|
86
|
+
@data = deep_dup(DEFAULTS)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load
|
|
90
|
+
path = root.join("reqcord.yml")
|
|
91
|
+
|
|
92
|
+
return self unless path.exist?
|
|
93
|
+
|
|
94
|
+
raw = YAML.safe_load_file(
|
|
95
|
+
path,
|
|
96
|
+
aliases: false
|
|
97
|
+
) || {}
|
|
98
|
+
|
|
99
|
+
unless raw.is_a?(Hash)
|
|
100
|
+
raise ConfigurationError,
|
|
101
|
+
"reqcord.yml must contain a YAML object"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@data = deep_merge(@data, stringify_keys(raw))
|
|
105
|
+
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_framework
|
|
110
|
+
ENV["REQCORD_TEST_FRAMEWORK"] ||
|
|
111
|
+
data.dig("test", "framework")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# An explicit command wins; nil means "build one from test.paths".
|
|
115
|
+
def test_command
|
|
116
|
+
value = ENV["REQCORD_TEST_COMMAND"] || data.dig("test", "command")
|
|
117
|
+
|
|
118
|
+
value.to_s.empty? ? nil : value.to_s
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Directories, files or globs the suite lives in; Reqcord picks the runner.
|
|
122
|
+
def test_paths
|
|
123
|
+
Array(data.dig("test", "paths")).map(&:to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def route_prefix
|
|
127
|
+
data.dig("routes", "prefix")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def output_directory
|
|
131
|
+
value =
|
|
132
|
+
ENV["REQCORD_OUTPUT"] ||
|
|
133
|
+
data.dig("output", "directory") ||
|
|
134
|
+
"docs/api"
|
|
135
|
+
|
|
136
|
+
root.join(value)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def include_uncovered?
|
|
140
|
+
data.dig("output", "include_uncovered") == true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def exporters
|
|
144
|
+
Array(data["exporters"]).map(&:to_s)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def variables
|
|
148
|
+
(data["variables"] || {}).merge(
|
|
149
|
+
"base_url" => base_url
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def base_url
|
|
154
|
+
ENV["REQCORD_BASE_URL"] ||
|
|
155
|
+
data.dig("variables", "base_url") ||
|
|
156
|
+
"http://localhost:3000"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def sanitized_headers
|
|
160
|
+
data.dig("sanitize", "headers") || {}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def sanitized_body_keys
|
|
164
|
+
data.dig("sanitize", "body") || {}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def noisy_header?(key)
|
|
168
|
+
NOISY_HEADERS.include?(key.to_s.downcase)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def deep_merge(left, right)
|
|
174
|
+
left.merge(right) do |_key, old_value, new_value|
|
|
175
|
+
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
176
|
+
deep_merge(old_value, new_value)
|
|
177
|
+
else
|
|
178
|
+
new_value
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def deep_dup(value)
|
|
184
|
+
Marshal.load(Marshal.dump(value))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def stringify_keys(hash)
|
|
188
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
189
|
+
result[key.to_s] =
|
|
190
|
+
if value.is_a?(Hash)
|
|
191
|
+
stringify_keys(value)
|
|
192
|
+
else
|
|
193
|
+
value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class Dataset
|
|
5
|
+
# 2: endpoints carry `parameters` (path/query/body schemas), `responses`
|
|
6
|
+
# (one schema + example per status), `route_name` and `also_methods`.
|
|
7
|
+
SCHEMA_VERSION = 2
|
|
8
|
+
|
|
9
|
+
# An endpoint page must not overwrite the resource or dataset pages.
|
|
10
|
+
RESERVED_BASENAMES = {
|
|
11
|
+
"index" => "list",
|
|
12
|
+
"readme" => "readme-endpoint",
|
|
13
|
+
"dataset" => "dataset-endpoint"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Endpoints served by one controller, as the exporters walk them. Keyed by
|
|
17
|
+
# the full controller path so `admin/users` and `api/v1/users` stay apart.
|
|
18
|
+
class Resource
|
|
19
|
+
attr_reader :name, :endpoints
|
|
20
|
+
|
|
21
|
+
def initialize(name)
|
|
22
|
+
@name = name.to_s
|
|
23
|
+
@endpoints = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def segments
|
|
27
|
+
name.split("/")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def short_name
|
|
31
|
+
segments.last.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def namespace
|
|
35
|
+
segments[0...-1].join("/")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Nested directories / folders: api/v2/customers.
|
|
39
|
+
def slug
|
|
40
|
+
segments.map { |segment| Support.parameterize(segment) }.join("/")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def title
|
|
44
|
+
Support.titleize(short_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def api_versions
|
|
48
|
+
endpoints.map(&:api_version).compact.uniq.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One stable file name per endpoint. Two routes sharing an action
|
|
52
|
+
# (`match … via: [:get, :post]`) are told apart by verb, never by a
|
|
53
|
+
# bare counter.
|
|
54
|
+
def file_basenames
|
|
55
|
+
bases = endpoints.to_h { |endpoint| [endpoint, RESERVED_BASENAMES.fetch(endpoint.slug, endpoint.slug)] }
|
|
56
|
+
shared = bases.values.tally
|
|
57
|
+
seen = Hash.new(0)
|
|
58
|
+
|
|
59
|
+
bases.to_h do |endpoint, base|
|
|
60
|
+
basename = shared[base] > 1 ? "#{base}-#{endpoint.method.downcase}" : base
|
|
61
|
+
seen[basename] += 1
|
|
62
|
+
basename = "#{basename}-#{seen[basename]}" if seen[basename] > 1
|
|
63
|
+
|
|
64
|
+
[endpoint, basename]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Rails emits both PATCH and PUT for `update`; one page serves both. The
|
|
70
|
+
# verb a test used wins, PATCH when neither or both did.
|
|
71
|
+
def self.fold_method_twins(endpoints)
|
|
72
|
+
groups = endpoints.group_by { |endpoint| [endpoint.path, endpoint.controller, endpoint.action] }
|
|
73
|
+
|
|
74
|
+
endpoints.filter_map do |endpoint|
|
|
75
|
+
next endpoint unless %w[PATCH PUT].include?(endpoint.method)
|
|
76
|
+
|
|
77
|
+
twins = groups.fetch([endpoint.path, endpoint.controller, endpoint.action])
|
|
78
|
+
.select { |candidate| %w[PATCH PUT].include?(candidate.method) }
|
|
79
|
+
|
|
80
|
+
next endpoint unless twins.size == 2
|
|
81
|
+
|
|
82
|
+
primary = twins.find(&:curl_ready?) || twins.find { |candidate| candidate.method == "PATCH" }
|
|
83
|
+
|
|
84
|
+
next nil unless endpoint.equal?(primary)
|
|
85
|
+
|
|
86
|
+
twin = twins.find { |candidate| !candidate.equal?(primary) }
|
|
87
|
+
twin.request_examples.each { |example| primary.add_request_example(example) }
|
|
88
|
+
twin.response_examples.each { |example| primary.add_response_example(example) }
|
|
89
|
+
primary.also_methods = [twin.method]
|
|
90
|
+
|
|
91
|
+
primary
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
attr_reader :schema_version, :endpoints
|
|
96
|
+
|
|
97
|
+
def initialize(
|
|
98
|
+
schema_version: SCHEMA_VERSION,
|
|
99
|
+
endpoints: []
|
|
100
|
+
)
|
|
101
|
+
@schema_version = schema_version
|
|
102
|
+
@endpoints = endpoints
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add(endpoint)
|
|
106
|
+
endpoints << endpoint
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def empty?
|
|
110
|
+
endpoints.empty?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Endpoints for which Reqcord captured at least one successful 2xx
|
|
114
|
+
# request. These are safe to use as canonical cURL/documentation examples.
|
|
115
|
+
def curl_ready_endpoints
|
|
116
|
+
endpoints.select(&:curl_ready?)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Backwards-compatible alias for callers that previously asked for
|
|
120
|
+
# documented endpoints. In the cURL-first MVP, documented means a
|
|
121
|
+
# successful request was actually captured.
|
|
122
|
+
def documented_endpoints
|
|
123
|
+
curl_ready_endpoints
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def uncovered_endpoints
|
|
127
|
+
endpoints.reject(&:curl_ready?)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def resources
|
|
131
|
+
grouped = endpoints.each_with_object({}) do |endpoint, memo|
|
|
132
|
+
key = endpoint.controller.to_s.empty? ? endpoint.resource : endpoint.controller
|
|
133
|
+
resource = memo[key] ||= Resource.new(key)
|
|
134
|
+
resource.endpoints << endpoint
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
grouped.values.sort_by(&:name).each do |resource|
|
|
138
|
+
resource.endpoints.sort_by! { |endpoint| [endpoint.path, endpoint.method] }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def to_h
|
|
143
|
+
{
|
|
144
|
+
schema_version: schema_version,
|
|
145
|
+
generated_at: Time.now.utc.iso8601,
|
|
146
|
+
endpoints: curl_ready_endpoints.map(&:to_h),
|
|
147
|
+
uncovered_routes: uncovered_endpoints.map do |endpoint|
|
|
148
|
+
{
|
|
149
|
+
name: endpoint.name,
|
|
150
|
+
method: endpoint.method,
|
|
151
|
+
path: endpoint.path,
|
|
152
|
+
controller: endpoint.controller,
|
|
153
|
+
action: endpoint.action,
|
|
154
|
+
resource: endpoint.resource,
|
|
155
|
+
api_version: endpoint.api_version,
|
|
156
|
+
route_name: endpoint.route_name,
|
|
157
|
+
also_methods: endpoint.also_methods
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def write(path)
|
|
164
|
+
path = Pathname(path)
|
|
165
|
+
|
|
166
|
+
FileUtils.mkdir_p(path.dirname)
|
|
167
|
+
|
|
168
|
+
File.write(
|
|
169
|
+
path,
|
|
170
|
+
JSON.pretty_generate(to_h)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
path.to_s
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class Endpoint
|
|
5
|
+
ACTION_TITLES = {
|
|
6
|
+
"index" => "List %<plural>s",
|
|
7
|
+
"show" => "Get %<singular>s",
|
|
8
|
+
"create" => "Create %<singular>s",
|
|
9
|
+
"update" => "Update %<singular>s",
|
|
10
|
+
"destroy" => "Delete %<singular>s",
|
|
11
|
+
"new" => "New %<singular>s",
|
|
12
|
+
"edit" => "Edit %<singular>s"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# One status the endpoint was seen to return, described by every body
|
|
16
|
+
# captured with that status; the first capture stands as the example.
|
|
17
|
+
Response = Struct.new(:status, :schema, :examples, keyword_init: true) do
|
|
18
|
+
def example
|
|
19
|
+
examples.first
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
status: status,
|
|
25
|
+
schema: schema.to_a,
|
|
26
|
+
example: example.body,
|
|
27
|
+
headers: example.headers,
|
|
28
|
+
content_type: example.content_type
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_accessor :name,
|
|
34
|
+
:method,
|
|
35
|
+
:path,
|
|
36
|
+
:controller,
|
|
37
|
+
:action,
|
|
38
|
+
:resource,
|
|
39
|
+
:api_version,
|
|
40
|
+
:route_name,
|
|
41
|
+
:also_methods,
|
|
42
|
+
:request_examples,
|
|
43
|
+
:response_examples
|
|
44
|
+
|
|
45
|
+
def initialize(
|
|
46
|
+
method:,
|
|
47
|
+
path:,
|
|
48
|
+
controller:,
|
|
49
|
+
action:,
|
|
50
|
+
name: nil,
|
|
51
|
+
resource: nil,
|
|
52
|
+
api_version: nil,
|
|
53
|
+
route_name: nil,
|
|
54
|
+
also_methods: [],
|
|
55
|
+
request_examples: [],
|
|
56
|
+
response_examples: []
|
|
57
|
+
)
|
|
58
|
+
@method = method.to_s.upcase
|
|
59
|
+
@path = path
|
|
60
|
+
@controller = controller
|
|
61
|
+
@action = action
|
|
62
|
+
@resource = resource || infer_resource(controller)
|
|
63
|
+
@api_version = api_version
|
|
64
|
+
@route_name = route_name
|
|
65
|
+
@also_methods = also_methods
|
|
66
|
+
@request_examples = request_examples
|
|
67
|
+
@response_examples = response_examples
|
|
68
|
+
@name = name || default_name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def http_method
|
|
72
|
+
method
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def key
|
|
76
|
+
"#{method} #{path}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A request only counts as successful through the status it received, so
|
|
80
|
+
# the pair is what tells us; callers should not have to set it by hand.
|
|
81
|
+
def add_exchange(request:, response:)
|
|
82
|
+
request.response_status ||= response.status if request && response
|
|
83
|
+
|
|
84
|
+
add_request_example(request)
|
|
85
|
+
add_response_example(response)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_request_example(example)
|
|
89
|
+
return if example.nil?
|
|
90
|
+
return if request_examples.any? { |candidate| candidate.signature == example.signature }
|
|
91
|
+
|
|
92
|
+
reset_schemas!
|
|
93
|
+
request_examples << example
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_response_example(example)
|
|
97
|
+
return if example.nil?
|
|
98
|
+
return if response_examples.any? { |candidate| candidate.signature == example.signature }
|
|
99
|
+
|
|
100
|
+
response_examples << example
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def documented?
|
|
104
|
+
!request_examples.empty? || !response_examples.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# cURL examples must come from a request that the application actually
|
|
108
|
+
# accepted. Error-case payloads are valuable response examples, but they
|
|
109
|
+
# must never become the endpoint's canonical request example.
|
|
110
|
+
def primary_request_example
|
|
111
|
+
successful_request_examples.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def curl_ready?
|
|
115
|
+
!primary_request_example.nil?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def successful_request_examples
|
|
119
|
+
request_examples.select(&:successful?)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def documented_request_examples
|
|
123
|
+
successful = successful_request_examples
|
|
124
|
+
successful.empty? ? request_examples : successful
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def responses_by_status
|
|
128
|
+
response_examples.group_by(&:status).sort_by { |status, _| status }.to_h
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Every status seen, each with a schema inferred from all of its bodies.
|
|
132
|
+
# Non-JSON bodies (plain text, HTML) carry no fields to describe.
|
|
133
|
+
def responses
|
|
134
|
+
responses_by_status.map do |status, examples|
|
|
135
|
+
bodies = examples.map(&:body).select { |body| body.is_a?(Hash) || body.is_a?(Array) }
|
|
136
|
+
|
|
137
|
+
Response.new(status: status, schema: Schema.infer(bodies, repetition: false), examples: examples)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# What the endpoint accepts, described only by requests the application
|
|
142
|
+
# accepted: a rejected payload says what the API refuses, not what it takes.
|
|
143
|
+
def body_schema
|
|
144
|
+
@body_schema ||= Schema.infer(successful_request_examples.map(&:body))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def query_schema
|
|
148
|
+
@query_schema ||= Schema.infer(successful_request_examples.map(&:query_params))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def path_param_schema
|
|
152
|
+
@path_param_schema ||= Schema.infer(successful_request_examples.map(&:path_params))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A member route addresses one record: a required `:param` follows the
|
|
156
|
+
# resource segment. Optional groups (`/items(/:id)`) and globs
|
|
157
|
+
# (`/files/*path`) do not make a route a member route.
|
|
158
|
+
def member?
|
|
159
|
+
segments = required_path.split("/").reject(&:empty?)
|
|
160
|
+
singular = Support.singularize(resource.to_s)
|
|
161
|
+
index = segments.rindex { |segment| segment == resource.to_s || segment == singular }
|
|
162
|
+
|
|
163
|
+
# `resource :cart` is served by CartsController at /cart: one record,
|
|
164
|
+
# so its custom actions (/cart/checkout) address that one record.
|
|
165
|
+
return true if index && segments[index] == singular && singular != resource.to_s
|
|
166
|
+
|
|
167
|
+
candidates = index ? segments[(index + 1)..] : segments
|
|
168
|
+
|
|
169
|
+
candidates.any? { |segment| segment.start_with?(":") }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Every dynamic segment, including globs and those inside optional groups.
|
|
173
|
+
def path_params
|
|
174
|
+
path.scan(/[:*]([a-zA-Z_][a-zA-Z0-9_]*)/).flatten
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# The path with its optional groups removed: what a request must carry.
|
|
178
|
+
def required_path
|
|
179
|
+
stripped = path.to_s
|
|
180
|
+
|
|
181
|
+
stripped = stripped.gsub(/\([^()]*\)/, "") while stripped.match?(/\([^()]*\)/)
|
|
182
|
+
|
|
183
|
+
stripped
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def slug
|
|
187
|
+
Support.parameterize(action.to_s.empty? ? key : action)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def to_h
|
|
191
|
+
{
|
|
192
|
+
name: name,
|
|
193
|
+
method: method,
|
|
194
|
+
path: path,
|
|
195
|
+
controller: controller,
|
|
196
|
+
action: action,
|
|
197
|
+
resource: resource,
|
|
198
|
+
api_version: api_version,
|
|
199
|
+
route_name: route_name,
|
|
200
|
+
also_methods: also_methods,
|
|
201
|
+
path_params: path_params,
|
|
202
|
+
parameters: {
|
|
203
|
+
path: path_param_schema.to_a,
|
|
204
|
+
query: query_schema.to_a,
|
|
205
|
+
body: body_schema.to_a
|
|
206
|
+
},
|
|
207
|
+
responses: responses.map(&:to_h),
|
|
208
|
+
request_examples: request_examples.map(&:to_h),
|
|
209
|
+
response_examples: response_examples.map(&:to_h)
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def reset_schemas!
|
|
216
|
+
@body_schema = nil
|
|
217
|
+
@query_schema = nil
|
|
218
|
+
@path_param_schema = nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def infer_resource(controller)
|
|
222
|
+
controller.to_s.split("/").last
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def default_name
|
|
226
|
+
label = resource.to_s.empty? ? "resource" : resource.to_s.tr("/", " ")
|
|
227
|
+
plural = Support.titleize(Support.pluralize(label))
|
|
228
|
+
singular = Support.titleize(Support.singularize(label))
|
|
229
|
+
|
|
230
|
+
# `root to: "home#index"` is a page, not a list of homes.
|
|
231
|
+
return singular if action.to_s == "index" && !resource_in_path?
|
|
232
|
+
|
|
233
|
+
template = ACTION_TITLES[action.to_s]
|
|
234
|
+
return format(template, plural: plural, singular: singular) if template
|
|
235
|
+
return plural if action.to_s.empty?
|
|
236
|
+
|
|
237
|
+
# `post "auth/login", to: "auth#login"`: the action is the page, the
|
|
238
|
+
# controller is only where it lives — "Login", not "Login Auths".
|
|
239
|
+
return Support.titleize(action) if singular_resource? && action_in_path?
|
|
240
|
+
|
|
241
|
+
"#{Support.titleize(action)} #{member? || singular_resource? ? singular : plural}".strip
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A controller named for one thing (auth, home, health) rather than a
|
|
245
|
+
# collection (customers).
|
|
246
|
+
def singular_resource?
|
|
247
|
+
Support.pluralize(resource.to_s) != resource.to_s
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def action_in_path?
|
|
251
|
+
required_path.split("/").last.to_s == action.to_s
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Whether the path itself names the resource (/customers, /cart), as
|
|
255
|
+
# opposed to a route like `/` or `/dashboard` served by some controller.
|
|
256
|
+
def resource_in_path?
|
|
257
|
+
static = required_path.split("/").reject { |segment| segment.empty? || segment.start_with?(":", "*") }
|
|
258
|
+
names = [resource.to_s, Support.singularize(resource.to_s)]
|
|
259
|
+
|
|
260
|
+
static.any? { |segment| names.include?(segment) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Exporters
|
|
5
|
+
# Writes one runnable .sh file per covered endpoint. The command is always
|
|
6
|
+
# rendered from the endpoint's successful captured request when available.
|
|
7
|
+
class Curl
|
|
8
|
+
def self.call(dataset:, output_dir:, configuration:)
|
|
9
|
+
new(
|
|
10
|
+
dataset: dataset,
|
|
11
|
+
output_dir: output_dir,
|
|
12
|
+
configuration: configuration
|
|
13
|
+
).call
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(dataset:, output_dir:, configuration:)
|
|
17
|
+
@dataset = dataset
|
|
18
|
+
@output_dir = Pathname(output_dir).join("curl")
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
FileUtils.mkdir_p(output_dir)
|
|
24
|
+
|
|
25
|
+
dataset.resources.flat_map do |resource|
|
|
26
|
+
basenames = resource.file_basenames
|
|
27
|
+
|
|
28
|
+
resource.endpoints.filter_map do |endpoint|
|
|
29
|
+
next unless endpoint.curl_ready?
|
|
30
|
+
|
|
31
|
+
example = endpoint.primary_request_example
|
|
32
|
+
|
|
33
|
+
directory = output_dir.join(resource.slug)
|
|
34
|
+
FileUtils.mkdir_p(directory)
|
|
35
|
+
|
|
36
|
+
path = directory.join("#{basenames.fetch(endpoint)}.sh")
|
|
37
|
+
File.write(path, script(endpoint, example))
|
|
38
|
+
File.chmod(0o755, path)
|
|
39
|
+
|
|
40
|
+
path.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :dataset, :output_dir, :configuration
|
|
48
|
+
|
|
49
|
+
def script(endpoint, example)
|
|
50
|
+
command = Renderers::Curl.call(
|
|
51
|
+
example,
|
|
52
|
+
base_url: configuration.base_url
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
<<~SH
|
|
56
|
+
#!/usr/bin/env bash
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
|
|
59
|
+
# #{endpoint.name}
|
|
60
|
+
# #{endpoint.method} #{endpoint.path}
|
|
61
|
+
#{command}
|
|
62
|
+
SH
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
register("curl", Curl)
|
|
67
|
+
end
|
|
68
|
+
end
|