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,364 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Reqcord
|
|
8
|
+
# Runs the test suite in a subprocess with capture enabled, then turns the
|
|
9
|
+
# captured exchanges into documentation. Route collection happens here, in a
|
|
10
|
+
# process that already has the application booted.
|
|
11
|
+
class Generator
|
|
12
|
+
def self.call(
|
|
13
|
+
resources: [],
|
|
14
|
+
version: nil
|
|
15
|
+
)
|
|
16
|
+
new(
|
|
17
|
+
resources: resources,
|
|
18
|
+
version: version
|
|
19
|
+
).call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(resources:, version:, configuration: Reqcord.configuration)
|
|
23
|
+
@resources = Array(resources).map(&:to_s)
|
|
24
|
+
@version = version&.to_s
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
validate!
|
|
30
|
+
|
|
31
|
+
routes = collect_routes
|
|
32
|
+
|
|
33
|
+
capture_file = build_capture_file
|
|
34
|
+
|
|
35
|
+
run_tests(capture_file)
|
|
36
|
+
|
|
37
|
+
exchanges =
|
|
38
|
+
read_exchanges(capture_file)
|
|
39
|
+
|
|
40
|
+
dataset =
|
|
41
|
+
build_dataset(
|
|
42
|
+
routes,
|
|
43
|
+
exchanges
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
report(exchanges, dataset)
|
|
47
|
+
|
|
48
|
+
write_outputs(dataset)
|
|
49
|
+
|
|
50
|
+
dataset
|
|
51
|
+
ensure
|
|
52
|
+
FileUtils.rm_f(capture_file) if capture_file
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Paths that were captured but belong to no documented route. Kept so the
|
|
56
|
+
# run can say why a request did not turn into documentation.
|
|
57
|
+
attr_reader :unmatched_paths
|
|
58
|
+
|
|
59
|
+
# Routes seen in the table but not documentable, by reason (redirect,
|
|
60
|
+
# mount). Set by collect_routes; exposed so a run can be reconciled.
|
|
61
|
+
attr_accessor :skipped_routes
|
|
62
|
+
|
|
63
|
+
# The command that runs the suite. An explicit `test.command` is used as
|
|
64
|
+
# written (globs expanded, since no shell is involved); otherwise a runner
|
|
65
|
+
# is built from `test.paths`: `bin/rails test` when the application has
|
|
66
|
+
# one, a plain Ruby runner when it does not, `rspec` for request specs.
|
|
67
|
+
def test_argv
|
|
68
|
+
explicit = configuration.test_command
|
|
69
|
+
return expand_globs(Shellwords.split(explicit)) if explicit
|
|
70
|
+
|
|
71
|
+
paths = configuration.test_paths
|
|
72
|
+
return %w[bin/rails test] if paths.empty?
|
|
73
|
+
|
|
74
|
+
case configuration.test_framework.to_s
|
|
75
|
+
when "rspec"
|
|
76
|
+
["rspec", *paths]
|
|
77
|
+
else
|
|
78
|
+
if configuration.root.join("bin", "rails").exist?
|
|
79
|
+
["bin/rails", "test", *paths]
|
|
80
|
+
else
|
|
81
|
+
["ruby", "-Itest", "-e", "ARGV.each { |file| require File.expand_path(file) }", *test_files(paths)]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Builds the dataset from exchanges that were captured earlier, without
|
|
87
|
+
# running the suite again.
|
|
88
|
+
def build_dataset(routes, exchanges)
|
|
89
|
+
endpoints = {}
|
|
90
|
+
@unmatched_paths = []
|
|
91
|
+
|
|
92
|
+
# A route answering any verb is documented once per verb a test used.
|
|
93
|
+
routes.each do |route|
|
|
94
|
+
endpoints[[route, route.method]] = route.endpoint unless route.any_verb?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
exchanges.each do |raw_exchange|
|
|
98
|
+
sanitized =
|
|
99
|
+
Sanitizers::Sanitizer.call(
|
|
100
|
+
raw_exchange,
|
|
101
|
+
configuration: configuration
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
route = find_route(routes, sanitized)
|
|
105
|
+
request = sanitized.fetch("request")
|
|
106
|
+
|
|
107
|
+
unless route
|
|
108
|
+
@unmatched_paths << "#{request['method']} #{request['path']}"
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
verb = route.any_verb? ? request.fetch("method").to_s.upcase : route.method
|
|
113
|
+
endpoint = endpoints[[route, verb]] ||= route.endpoint(method: verb)
|
|
114
|
+
|
|
115
|
+
attach_exchange(endpoint, sanitized)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
routes.select(&:any_verb?).each do |route|
|
|
119
|
+
next if endpoints.keys.any? { |(seen, _verb)| seen == route }
|
|
120
|
+
|
|
121
|
+
endpoints[[route, RouteCollector::ANY]] = route.endpoint
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Dataset.new(
|
|
125
|
+
endpoints: Dataset.fold_method_twins(endpoints.values)
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
attr_reader :resources,
|
|
132
|
+
:version,
|
|
133
|
+
:configuration
|
|
134
|
+
|
|
135
|
+
# Silence here is the worst outcome: an empty page set looks the same
|
|
136
|
+
# whether the API has no tests or the capture never ran.
|
|
137
|
+
def report(exchanges, dataset)
|
|
138
|
+
if exchanges.empty?
|
|
139
|
+
Reqcord.warn("no request was captured")
|
|
140
|
+
Reqcord.warn(" is reqcord in the :test group of your Gemfile, and does `test.command` run integration tests?")
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
covered = dataset.curl_ready_endpoints.size
|
|
145
|
+
uncovered = dataset.uncovered_endpoints.size
|
|
146
|
+
matched = exchanges.size - unmatched_paths.size
|
|
147
|
+
skipped = skipped_routes || {}
|
|
148
|
+
skipped_total = skipped.values.sum
|
|
149
|
+
|
|
150
|
+
Reqcord.log("captured #{exchanges.size} request(s), #{matched} matched a documented route")
|
|
151
|
+
Reqcord.log("captured a successful 2xx request for #{covered} of #{dataset.endpoints.size} endpoint(s)")
|
|
152
|
+
|
|
153
|
+
# Every route lands in exactly one bucket; the sum is the proof.
|
|
154
|
+
Reqcord.log(
|
|
155
|
+
"routes: #{dataset.endpoints.size + skipped_total} = " \
|
|
156
|
+
"#{covered} documented + #{uncovered} uncovered + #{skipped_total} skipped"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if skipped_total.positive?
|
|
160
|
+
reasons = skipped.map { |reason, count| "#{count} #{reason}" }.join(", ")
|
|
161
|
+
Reqcord.log("skipped #{skipped_total} route(s) that cannot be documented: #{reasons}")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
return if unmatched_paths.empty?
|
|
165
|
+
|
|
166
|
+
shown = unmatched_paths.uniq.first(5)
|
|
167
|
+
|
|
168
|
+
Reqcord.log("#{unmatched_paths.uniq.size} path(s) matched no documented route, for example:")
|
|
169
|
+
shown.each { |path| Reqcord.log(" #{path}") }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
FRAMEWORKS = %w[minitest rspec].freeze
|
|
173
|
+
|
|
174
|
+
def validate!
|
|
175
|
+
unless FRAMEWORKS.include?(configuration.test_framework.to_s)
|
|
176
|
+
raise ConfigurationError,
|
|
177
|
+
"unsupported test framework: #{configuration.test_framework.inspect}, " \
|
|
178
|
+
"expected one of #{FRAMEWORKS.join(', ')}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
configuration.exporters.each { |name| Exporters.fetch(name) }
|
|
182
|
+
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def collect_routes
|
|
187
|
+
collector = RouteCollector.new(
|
|
188
|
+
resources: resources,
|
|
189
|
+
version: version,
|
|
190
|
+
prefix: configuration.route_prefix
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
routes = collector.call
|
|
194
|
+
@skipped_routes = collector.skipped
|
|
195
|
+
|
|
196
|
+
routes
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# A directory means every *_test.rb (or *_spec.rb) beneath it.
|
|
200
|
+
def test_files(paths)
|
|
201
|
+
pattern = configuration.test_framework.to_s == "rspec" ? "*_spec.rb" : "*_test.rb"
|
|
202
|
+
|
|
203
|
+
paths.flat_map do |path|
|
|
204
|
+
if configuration.root.join(path).directory?
|
|
205
|
+
Dir.glob(File.join(path, "**", pattern), base: configuration.root.to_s).sort
|
|
206
|
+
else
|
|
207
|
+
expand_globs([path])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def expand_globs(args)
|
|
213
|
+
args.flat_map do |arg|
|
|
214
|
+
next [arg] unless arg.match?(/[*?\[{]/)
|
|
215
|
+
|
|
216
|
+
matches = Dir.glob(arg, base: configuration.root.to_s).sort
|
|
217
|
+
matches.empty? ? [arg] : matches
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_capture_file
|
|
222
|
+
Reqcord.root.join(
|
|
223
|
+
"tmp",
|
|
224
|
+
"reqcord",
|
|
225
|
+
"#{SecureRandom.hex(12)}.ndjson"
|
|
226
|
+
).to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def run_tests(capture_file)
|
|
230
|
+
FileUtils.mkdir_p(
|
|
231
|
+
File.dirname(capture_file)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
env = {
|
|
235
|
+
"REQCORD_CAPTURE" => "1",
|
|
236
|
+
"REQCORD_CAPTURE_FILE" => capture_file
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
command = test_argv
|
|
240
|
+
|
|
241
|
+
Reqcord.log("running #{Shellwords.join(command)}")
|
|
242
|
+
|
|
243
|
+
# Streamed rather than captured: a long suite should print as it runs.
|
|
244
|
+
status =
|
|
245
|
+
Open3.popen2e(
|
|
246
|
+
env,
|
|
247
|
+
*command,
|
|
248
|
+
chdir: Reqcord.root.to_s
|
|
249
|
+
) do |stdin, output, wait_thread|
|
|
250
|
+
stdin.close
|
|
251
|
+
|
|
252
|
+
output.each_line { |line| $stdout.print(line) }
|
|
253
|
+
|
|
254
|
+
wait_thread.value
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
return if status.success?
|
|
258
|
+
|
|
259
|
+
raise GenerationError,
|
|
260
|
+
"Test suite failed while generating Reqcord documentation"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def read_exchanges(capture_file)
|
|
264
|
+
return [] unless File.exist?(capture_file)
|
|
265
|
+
|
|
266
|
+
File.readlines(capture_file)
|
|
267
|
+
.filter_map do |line|
|
|
268
|
+
line = line.strip
|
|
269
|
+
|
|
270
|
+
next if line.empty?
|
|
271
|
+
|
|
272
|
+
begin
|
|
273
|
+
JSON.parse(line)
|
|
274
|
+
rescue JSON::ParserError
|
|
275
|
+
Reqcord.warn("skipping malformed capture line")
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def find_route(routes, exchange)
|
|
282
|
+
request = exchange.fetch("request")
|
|
283
|
+
method = request.fetch("method")
|
|
284
|
+
path = request.fetch("path")
|
|
285
|
+
|
|
286
|
+
routes.find { |route| route.matches?(method, path) }
|
|
287
|
+
rescue StandardError => e
|
|
288
|
+
raise GenerationError, "could not match #{method} #{path} against the route table: #{e.message}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def attach_exchange(endpoint, exchange)
|
|
292
|
+
request_data = exchange.fetch("request")
|
|
293
|
+
response_data = exchange.fetch("response")
|
|
294
|
+
source = normalize_source(exchange["source"])
|
|
295
|
+
status = response_data.fetch("status")
|
|
296
|
+
|
|
297
|
+
request =
|
|
298
|
+
RequestExample.new(
|
|
299
|
+
name: example_name(source),
|
|
300
|
+
method: request_data.fetch("method"),
|
|
301
|
+
# The concrete path, not the route pattern: the cURL must be runnable.
|
|
302
|
+
path: request_data.fetch("path"),
|
|
303
|
+
path_params: request_data["path_params"] || {},
|
|
304
|
+
query_params: request_data["query_params"] || {},
|
|
305
|
+
headers: request_data["headers"] || {},
|
|
306
|
+
body: request_data["body"],
|
|
307
|
+
content_type: request_data["content_type"],
|
|
308
|
+
response_status: status,
|
|
309
|
+
source: source
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
response =
|
|
313
|
+
ResponseExample.new(
|
|
314
|
+
name: example_name(source),
|
|
315
|
+
status: status,
|
|
316
|
+
headers: response_data["headers"] || {},
|
|
317
|
+
body: response_data["body"],
|
|
318
|
+
content_type: response_data["content_type"],
|
|
319
|
+
source: source
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
endpoint.add_exchange(
|
|
323
|
+
request: request,
|
|
324
|
+
response: response
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# "test_creates_customer" -> "Creates Customer"
|
|
329
|
+
def example_name(source)
|
|
330
|
+
test = source["test"].to_s.sub(/\Atest_/, "")
|
|
331
|
+
|
|
332
|
+
test.empty? ? nil : Support.titleize(test)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def normalize_source(source)
|
|
336
|
+
source = (source || {}).transform_keys(&:to_s)
|
|
337
|
+
file = source["file"].to_s
|
|
338
|
+
prefix = "#{Reqcord.root}/"
|
|
339
|
+
|
|
340
|
+
source["file"] = file.delete_prefix(prefix) unless file.empty?
|
|
341
|
+
source.compact
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def write_outputs(dataset)
|
|
345
|
+
output = configuration.output_directory
|
|
346
|
+
|
|
347
|
+
FileUtils.mkdir_p(output)
|
|
348
|
+
|
|
349
|
+
written = [dataset.write(output.join("dataset.json"))]
|
|
350
|
+
|
|
351
|
+
configuration.exporters.each do |name|
|
|
352
|
+
written.concat(
|
|
353
|
+
Exporters.fetch(name).call(
|
|
354
|
+
dataset: dataset,
|
|
355
|
+
output_dir: output,
|
|
356
|
+
configuration: configuration
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
written
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
rake_tasks do
|
|
6
|
+
load File.expand_path(
|
|
7
|
+
"../tasks/reqcord.rake",
|
|
8
|
+
__dir__
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "reqcord.capture" do
|
|
13
|
+
next unless Reqcord::Capture.enabled?
|
|
14
|
+
|
|
15
|
+
require "action_dispatch/testing/integration"
|
|
16
|
+
require_relative "capture/integration_patch"
|
|
17
|
+
|
|
18
|
+
unless ActionDispatch::Integration::Session <
|
|
19
|
+
Reqcord::Capture::IntegrationPatch
|
|
20
|
+
|
|
21
|
+
ActionDispatch::Integration::Session.prepend(
|
|
22
|
+
Reqcord::Capture::IntegrationPatch
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Capture itself is framework agnostic; only naming the example needs to
|
|
27
|
+
# know which test framework is running.
|
|
28
|
+
case Reqcord.configuration.test_framework.to_s
|
|
29
|
+
when "minitest"
|
|
30
|
+
require "minitest/test"
|
|
31
|
+
require_relative "capture/minitest_context"
|
|
32
|
+
|
|
33
|
+
unless Minitest::Test < Reqcord::Capture::MinitestContext
|
|
34
|
+
Minitest::Test.prepend(
|
|
35
|
+
Reqcord::Capture::MinitestContext
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
when "rspec"
|
|
40
|
+
require "rspec/core"
|
|
41
|
+
require_relative "capture/rspec_context"
|
|
42
|
+
|
|
43
|
+
Reqcord::Capture::RSpecContext.install!
|
|
44
|
+
|
|
45
|
+
else
|
|
46
|
+
raise ConfigurationError,
|
|
47
|
+
"unsupported test framework: #{Reqcord.configuration.test_framework.inspect}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Renderers
|
|
5
|
+
class Curl
|
|
6
|
+
def self.call(request, base_url:)
|
|
7
|
+
new(
|
|
8
|
+
request,
|
|
9
|
+
base_url: base_url
|
|
10
|
+
).call
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(request, base_url:)
|
|
14
|
+
@request = request
|
|
15
|
+
@base_url = base_url.to_s.sub(%r{/$}, "")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
parts = [
|
|
20
|
+
"curl --request #{request.method}",
|
|
21
|
+
%(--url "#{url}")
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
request.headers.each do |key, value|
|
|
25
|
+
parts << %(--header "#{key}: #{escape_header(value)}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parts << data_argument if request.body?
|
|
29
|
+
|
|
30
|
+
parts.join(" \\\n ")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :request, :base_url
|
|
36
|
+
|
|
37
|
+
def url
|
|
38
|
+
"#{base_url}#{Payload.path_with_query(request)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def data_argument
|
|
42
|
+
"--data '#{shell_single_quote(Payload.raw_body(request))}'"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def shell_single_quote(value)
|
|
46
|
+
value.to_s.gsub("'", %q('"'"'))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def escape_header(value)
|
|
50
|
+
value.to_s
|
|
51
|
+
.gsub("\\") { "\\\\" }
|
|
52
|
+
.gsub('"') { '\\"' }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Renderers
|
|
5
|
+
# The wire-level decisions every output format has to agree on: whether a
|
|
6
|
+
# request is JSON, how a nested query flattens, what the body looks like
|
|
7
|
+
# as text. cURL, Postman and any later exporter read these, never their
|
|
8
|
+
# own copy.
|
|
9
|
+
module Payload
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def json?(request)
|
|
13
|
+
request.content_type.to_s.include?("json") ||
|
|
14
|
+
request.headers.any? do |key, value|
|
|
15
|
+
key.to_s.casecmp?("Content-Type") && value.to_s.include?("json")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Rails bracket notation: { filter: { status: "a" }, ids: [1, 2] } becomes
|
|
20
|
+
# [["filter[status]", "a"], ["ids[]", 1], ["ids[]", 2]].
|
|
21
|
+
def flatten_query(hash, prefix = nil)
|
|
22
|
+
hash.flat_map do |key, value|
|
|
23
|
+
current = prefix ? "#{prefix}[#{key}]" : key.to_s
|
|
24
|
+
|
|
25
|
+
case value
|
|
26
|
+
when Hash
|
|
27
|
+
flatten_query(value, current)
|
|
28
|
+
when Array
|
|
29
|
+
value.flat_map do |item|
|
|
30
|
+
if item.is_a?(Hash)
|
|
31
|
+
flatten_query(item, "#{current}[]")
|
|
32
|
+
else
|
|
33
|
+
[["#{current}[]", item]]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
[[current, value]]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def query_pairs(request)
|
|
43
|
+
flatten_query(request.query_params)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def form_pairs(request)
|
|
47
|
+
request.body.is_a?(Hash) ? flatten_query(request.body) : []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def path_with_query(request)
|
|
51
|
+
return request.path if request.query_params.empty?
|
|
52
|
+
|
|
53
|
+
"#{request.path}?#{URI.encode_www_form(query_pairs(request))}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The body as it goes on the wire: pretty JSON, a form string, or the
|
|
57
|
+
# raw text the test sent.
|
|
58
|
+
def raw_body(request)
|
|
59
|
+
if json?(request)
|
|
60
|
+
JSON.pretty_generate(request.body)
|
|
61
|
+
elsif request.body.is_a?(Hash)
|
|
62
|
+
URI.encode_www_form(form_pairs(request))
|
|
63
|
+
else
|
|
64
|
+
request.body.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class RequestExample
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
name
|
|
7
|
+
method
|
|
8
|
+
path
|
|
9
|
+
path_params
|
|
10
|
+
query_params
|
|
11
|
+
headers
|
|
12
|
+
body
|
|
13
|
+
content_type
|
|
14
|
+
response_status
|
|
15
|
+
source
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
attr_accessor(*ATTRIBUTES)
|
|
19
|
+
|
|
20
|
+
def initialize(
|
|
21
|
+
method:,
|
|
22
|
+
path:,
|
|
23
|
+
name: nil,
|
|
24
|
+
path_params: {},
|
|
25
|
+
query_params: {},
|
|
26
|
+
headers: {},
|
|
27
|
+
body: nil,
|
|
28
|
+
content_type: nil,
|
|
29
|
+
response_status: nil,
|
|
30
|
+
source: {}
|
|
31
|
+
)
|
|
32
|
+
@name = name
|
|
33
|
+
@method = method.to_s.upcase
|
|
34
|
+
@path = path
|
|
35
|
+
@path_params = path_params || {}
|
|
36
|
+
@query_params = query_params || {}
|
|
37
|
+
@headers = headers || {}
|
|
38
|
+
@body = body
|
|
39
|
+
@content_type = content_type
|
|
40
|
+
@response_status = response_status&.to_i
|
|
41
|
+
@source = source || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# `method` is the HTTP verb here; this alias keeps call sites that mean the
|
|
45
|
+
# verb from reading like reflection.
|
|
46
|
+
def http_method
|
|
47
|
+
method
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A payload of empty containers ({"experience" => {}}) carries nothing to
|
|
51
|
+
# document, and sending it as --data would only mislead.
|
|
52
|
+
def body?
|
|
53
|
+
meaningful?(body)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The request the endpoint page leads with should be one that worked.
|
|
57
|
+
def successful?
|
|
58
|
+
response_status.nil? ? false : (200..299).cover?(response_status)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Identical requests captured by several tests are stored once — but the
|
|
62
|
+
# same request that produced a different status is a different example.
|
|
63
|
+
# Sanitization can make a right and a wrong password look identical; the
|
|
64
|
+
# 200 must not be dropped because the 401 was captured first.
|
|
65
|
+
def signature
|
|
66
|
+
JSON.generate([method, path, headers, query_params, body, response_status])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
name: name,
|
|
72
|
+
method: method,
|
|
73
|
+
path: path,
|
|
74
|
+
path_params: path_params,
|
|
75
|
+
query_params: query_params,
|
|
76
|
+
headers: headers,
|
|
77
|
+
body: body,
|
|
78
|
+
content_type: content_type,
|
|
79
|
+
response_status: response_status,
|
|
80
|
+
source: source
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Tolerates keys this version does not know, so a dataset written by a
|
|
85
|
+
# newer Reqcord still loads.
|
|
86
|
+
def self.from_h(hash)
|
|
87
|
+
hash = hash.transform_keys(&:to_sym).slice(*ATTRIBUTES)
|
|
88
|
+
|
|
89
|
+
new(**hash)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def meaningful?(value)
|
|
95
|
+
case value
|
|
96
|
+
when nil then false
|
|
97
|
+
when Hash then value.any? { |_key, nested| meaningful?(nested) }
|
|
98
|
+
when Array then value.any? { |item| meaningful?(item) }
|
|
99
|
+
when String then !value.empty?
|
|
100
|
+
else true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
class ResponseExample
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
name
|
|
7
|
+
status
|
|
8
|
+
headers
|
|
9
|
+
body
|
|
10
|
+
content_type
|
|
11
|
+
source
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
attr_accessor(*ATTRIBUTES)
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
status:,
|
|
18
|
+
name: nil,
|
|
19
|
+
headers: {},
|
|
20
|
+
body: nil,
|
|
21
|
+
content_type: nil,
|
|
22
|
+
source: {}
|
|
23
|
+
)
|
|
24
|
+
@name = name
|
|
25
|
+
@status = status.to_i
|
|
26
|
+
@headers = headers || {}
|
|
27
|
+
@body = body
|
|
28
|
+
@content_type = content_type
|
|
29
|
+
@source = source || {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# "201 Created"
|
|
33
|
+
def status_text
|
|
34
|
+
Rack::Utils::HTTP_STATUS_CODES[status] || "Unknown"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def title
|
|
38
|
+
"#{status} #{status_text}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def body?
|
|
42
|
+
!(body.nil? || (body.respond_to?(:empty?) && body.empty?))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty_body?
|
|
46
|
+
!body?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def signature
|
|
50
|
+
JSON.generate([status, body])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
name: name,
|
|
56
|
+
status: status,
|
|
57
|
+
headers: headers,
|
|
58
|
+
body: body,
|
|
59
|
+
content_type: content_type,
|
|
60
|
+
source: source
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Tolerates keys this version does not know, so a dataset written by a
|
|
65
|
+
# newer Reqcord still loads.
|
|
66
|
+
def self.from_h(hash)
|
|
67
|
+
hash = hash.transform_keys(&:to_sym).slice(*ATTRIBUTES)
|
|
68
|
+
|
|
69
|
+
new(**hash)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|