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,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
# The documented surface is the route table, not the captured traffic: every
|
|
5
|
+
# matching route becomes an endpoint, and captures are attached to it. Routes
|
|
6
|
+
# no test exercised stay in the dataset as documentation gaps.
|
|
7
|
+
#
|
|
8
|
+
# Nothing is dropped silently: a route either becomes a Route, or is counted
|
|
9
|
+
# in `skipped` with the reason (redirect, Rack mount, Rails internal).
|
|
10
|
+
class RouteCollector
|
|
11
|
+
# A route that answers any verb (`via: :all`); the captured verb decides
|
|
12
|
+
# what gets documented.
|
|
13
|
+
ANY = "ANY"
|
|
14
|
+
|
|
15
|
+
Route = Struct.new(
|
|
16
|
+
:name,
|
|
17
|
+
:method,
|
|
18
|
+
:path,
|
|
19
|
+
:controller,
|
|
20
|
+
:action,
|
|
21
|
+
:resource,
|
|
22
|
+
:api_version,
|
|
23
|
+
:rails_route,
|
|
24
|
+
:mount_prefix,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def endpoint(method: self.method)
|
|
28
|
+
Endpoint.new(
|
|
29
|
+
method: method,
|
|
30
|
+
path: path,
|
|
31
|
+
controller: controller,
|
|
32
|
+
action: action,
|
|
33
|
+
resource: resource,
|
|
34
|
+
api_version: api_version,
|
|
35
|
+
route_name: name
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def any_verb?
|
|
40
|
+
method == ANY
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def matches?(request_method, request_path)
|
|
44
|
+
return false unless any_verb? || method == request_method.to_s.upcase
|
|
45
|
+
|
|
46
|
+
relative = relative_path(request_path)
|
|
47
|
+
return false if relative.nil?
|
|
48
|
+
|
|
49
|
+
!!rails_route.path.match(relative)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# An engine's pattern knows nothing about where it was mounted.
|
|
55
|
+
def relative_path(request_path)
|
|
56
|
+
return request_path if mount_prefix.nil? || mount_prefix.empty?
|
|
57
|
+
return nil unless request_path.start_with?(mount_prefix)
|
|
58
|
+
|
|
59
|
+
rest = request_path.delete_prefix(mount_prefix)
|
|
60
|
+
return nil unless rest.empty? || rest.start_with?("/")
|
|
61
|
+
|
|
62
|
+
rest.empty? ? "/" : rest
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.call(
|
|
67
|
+
resources: [],
|
|
68
|
+
version: nil,
|
|
69
|
+
prefix: nil,
|
|
70
|
+
route_set: nil
|
|
71
|
+
)
|
|
72
|
+
new(
|
|
73
|
+
resources: resources,
|
|
74
|
+
version: version,
|
|
75
|
+
prefix: prefix,
|
|
76
|
+
route_set: route_set
|
|
77
|
+
).call
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Routes that were seen but cannot be documented, by reason.
|
|
81
|
+
attr_reader :skipped
|
|
82
|
+
|
|
83
|
+
def initialize(resources:, version:, prefix:, route_set: nil)
|
|
84
|
+
@resources = Array(resources).map(&:to_s)
|
|
85
|
+
@version = version&.to_s
|
|
86
|
+
@prefix = prefix
|
|
87
|
+
@route_set = route_set
|
|
88
|
+
@skipped = Hash.new(0)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def call
|
|
92
|
+
@skipped = Hash.new(0)
|
|
93
|
+
|
|
94
|
+
collect(route_set.routes, mount_prefix: nil)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def skipped_count
|
|
98
|
+
skipped.values.sum
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
attr_reader :resources, :version, :prefix
|
|
104
|
+
|
|
105
|
+
def route_set
|
|
106
|
+
@route_set ||= Rails.application.routes
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def collect(rails_routes, mount_prefix:)
|
|
110
|
+
rails_routes.flat_map { |rails_route| build_routes(rails_route, mount_prefix) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_routes(rails_route, mount_prefix)
|
|
114
|
+
return [] if rails_route.internal
|
|
115
|
+
|
|
116
|
+
app = rails_route.app
|
|
117
|
+
spec = normalize_path(rails_route.path.spec.to_s)
|
|
118
|
+
|
|
119
|
+
# A mounted engine's routes live in its own table, relative to the mount.
|
|
120
|
+
if engine?(app)
|
|
121
|
+
return collect(app.rack_app.routes.routes, mount_prefix: join(mount_prefix, spec))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
controller = rails_route.defaults[:controller]&.to_s
|
|
125
|
+
action = rails_route.defaults[:action]&.to_s
|
|
126
|
+
|
|
127
|
+
if controller.nil? || action.nil?
|
|
128
|
+
@skipped[skip_reason(app)] += 1
|
|
129
|
+
return []
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
return [] if internal?(controller)
|
|
133
|
+
|
|
134
|
+
path = join(mount_prefix, spec)
|
|
135
|
+
|
|
136
|
+
return [] unless matches_prefix?(path)
|
|
137
|
+
return [] unless matches_version?(controller, path)
|
|
138
|
+
|
|
139
|
+
resource = controller.split("/").last
|
|
140
|
+
|
|
141
|
+
return [] unless matches_resource?(resource, controller)
|
|
142
|
+
|
|
143
|
+
normalize_methods(rails_route.verb).map do |method|
|
|
144
|
+
Route.new(
|
|
145
|
+
name: rails_route.name,
|
|
146
|
+
method: method,
|
|
147
|
+
path: path,
|
|
148
|
+
controller: controller,
|
|
149
|
+
action: action,
|
|
150
|
+
resource: resource,
|
|
151
|
+
api_version: detect_version(controller, path),
|
|
152
|
+
rails_route: rails_route,
|
|
153
|
+
mount_prefix: mount_prefix
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A mounted app that carries its own route table (a Rails::Engine, or
|
|
159
|
+
# anything shaped like one). Checked by shape rather than by class: the
|
|
160
|
+
# `Rails::Engine` constant need not be loaded, and a Sinatra app — whose
|
|
161
|
+
# `routes` is a plain Hash — is a Rack mount, not an engine.
|
|
162
|
+
def engine?(app)
|
|
163
|
+
return false unless app.respond_to?(:rack_app)
|
|
164
|
+
|
|
165
|
+
rack_app = app.rack_app
|
|
166
|
+
|
|
167
|
+
rack_app.respond_to?(:routes) && rack_app.routes.respond_to?(:routes)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def skip_reason(app)
|
|
171
|
+
rack_app = app.respond_to?(:rack_app) ? app.rack_app : app
|
|
172
|
+
|
|
173
|
+
if defined?(ActionDispatch::Routing::Redirect) && rack_app.is_a?(ActionDispatch::Routing::Redirect)
|
|
174
|
+
"redirect"
|
|
175
|
+
else
|
|
176
|
+
"mount"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Rails 8 exposes the verb as a plain string: "GET", "GET|POST" for
|
|
181
|
+
# `via: [:get, :post]`, "" for `via: :all`. Older versions used a regexp
|
|
182
|
+
# whose source carried anchors.
|
|
183
|
+
def normalize_methods(verb)
|
|
184
|
+
values = verb.to_s
|
|
185
|
+
.gsub(/[$^]/, "")
|
|
186
|
+
.split("|")
|
|
187
|
+
.map(&:strip)
|
|
188
|
+
.reject(&:empty?)
|
|
189
|
+
.map(&:upcase)
|
|
190
|
+
|
|
191
|
+
values.empty? ? [ANY] : values
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def normalize_path(path)
|
|
195
|
+
path.sub(/\(\.:format\)\z/, "")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def join(mount_prefix, path)
|
|
199
|
+
return path if mount_prefix.nil? || mount_prefix.empty?
|
|
200
|
+
return mount_prefix if path == "/"
|
|
201
|
+
|
|
202
|
+
"#{mount_prefix}#{path}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def internal?(controller)
|
|
206
|
+
controller.start_with?(
|
|
207
|
+
"rails/",
|
|
208
|
+
"active_storage/",
|
|
209
|
+
"action_mailbox/",
|
|
210
|
+
"turbo/"
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def matches_prefix?(path)
|
|
215
|
+
return true if prefix.nil? || prefix.empty?
|
|
216
|
+
|
|
217
|
+
path.start_with?(prefix)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# `RESOURCE=customers`, `RESOURCE=cart` (a singular resource is served by
|
|
221
|
+
# `carts`) and `RESOURCE=api/v2/customers` all name the same thing.
|
|
222
|
+
def matches_resource?(resource, controller)
|
|
223
|
+
return true if resources.empty?
|
|
224
|
+
|
|
225
|
+
candidates = [resource, controller, Support.singularize(resource)]
|
|
226
|
+
|
|
227
|
+
resources.any? { |wanted| candidates.include?(wanted) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def matches_version?(controller, path)
|
|
231
|
+
return true if version.nil? || version.empty?
|
|
232
|
+
|
|
233
|
+
controller.split("/").include?(version) ||
|
|
234
|
+
path.split("/").include?(version)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def detect_version(controller, path)
|
|
238
|
+
controller.split("/").find { |segment| segment.match?(/\Av\d+\z/) } ||
|
|
239
|
+
path.split("/").find { |segment| segment.match?(/\Av\d+\z/) }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Sanitizers
|
|
5
|
+
# Works on a raw captured exchange, before anything reaches the dataset:
|
|
6
|
+
# credentials must never be written to a generated file.
|
|
7
|
+
class Sanitizer
|
|
8
|
+
DEFAULT_SECRET_HEADERS = %w[
|
|
9
|
+
Authorization
|
|
10
|
+
Proxy-Authorization
|
|
11
|
+
Cookie
|
|
12
|
+
Set-Cookie
|
|
13
|
+
X-Api-Key
|
|
14
|
+
X-Auth-Token
|
|
15
|
+
X-Csrf-Token
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
SCHEME_PATTERN = /\A(Bearer|Token|Basic)\s+/i
|
|
19
|
+
|
|
20
|
+
def self.call(exchange, configuration:)
|
|
21
|
+
new(
|
|
22
|
+
exchange,
|
|
23
|
+
configuration: configuration
|
|
24
|
+
).call
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(exchange, configuration:)
|
|
28
|
+
@exchange = deep_dup(exchange)
|
|
29
|
+
@configuration = configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call
|
|
33
|
+
sanitize_headers!(
|
|
34
|
+
exchange.dig("request", "headers")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
sanitize_headers!(
|
|
38
|
+
exchange.dig("response", "headers"),
|
|
39
|
+
response: true
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
sanitize_body!("request")
|
|
43
|
+
sanitize_body!("response")
|
|
44
|
+
|
|
45
|
+
exchange
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :exchange, :configuration
|
|
51
|
+
|
|
52
|
+
def sanitize_headers!(headers, response: false)
|
|
53
|
+
return unless headers.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
replacements = configuration.sanitized_headers
|
|
56
|
+
|
|
57
|
+
headers.keys.each do |key|
|
|
58
|
+
if configuration.noisy_header?(key)
|
|
59
|
+
headers.delete(key)
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if blank?(headers[key])
|
|
64
|
+
headers.delete(key)
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
replacement = find_replacement(replacements, key)
|
|
69
|
+
|
|
70
|
+
if replacement
|
|
71
|
+
headers[key] = replacement
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
headers[key] = redact(key, headers[key]) if secret_header?(key)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sanitize_body!(side)
|
|
80
|
+
body = exchange.dig(side, "body")
|
|
81
|
+
|
|
82
|
+
return unless body.is_a?(Hash) || body.is_a?(Array)
|
|
83
|
+
|
|
84
|
+
exchange[side]["body"] = sanitize_value(body)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sanitize_value(value)
|
|
88
|
+
case value
|
|
89
|
+
when Hash
|
|
90
|
+
value.each_with_object({}) do |(key, nested), result|
|
|
91
|
+
replacement = body_replacement(key)
|
|
92
|
+
|
|
93
|
+
result[key] = replacement || sanitize_value(nested)
|
|
94
|
+
end
|
|
95
|
+
when Array
|
|
96
|
+
value.map { |item| sanitize_value(item) }
|
|
97
|
+
else
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def body_replacement(key)
|
|
103
|
+
configuration.sanitized_body_keys.find do |name, _value|
|
|
104
|
+
name.to_s.casecmp?(key.to_s)
|
|
105
|
+
end&.last
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def find_replacement(replacements, key)
|
|
109
|
+
pair = replacements.find do |header, _value|
|
|
110
|
+
header.to_s.casecmp?(key.to_s)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
pair&.last
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The scheme is kept so the generated cURL stays copy-pasteable.
|
|
117
|
+
def redact(key, value)
|
|
118
|
+
placeholder = "{{#{key.to_s.tr('-', '_').downcase}}}"
|
|
119
|
+
|
|
120
|
+
match = value.to_s.match(SCHEME_PATTERN)
|
|
121
|
+
|
|
122
|
+
match ? "#{match[1]} #{placeholder}" : placeholder
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def secret_header?(key)
|
|
126
|
+
DEFAULT_SECRET_HEADERS.any? do |header|
|
|
127
|
+
header.casecmp?(key.to_s)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def blank?(value)
|
|
132
|
+
value.nil? || value.to_s.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def deep_dup(value)
|
|
136
|
+
Marshal.load(Marshal.dump(value))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
# Describes what an endpoint accepts, inferred from the requests that
|
|
5
|
+
# actually worked. One test sends status "active", another "passive", a third
|
|
6
|
+
# sends "inactive" and gets a 422: the documentation should say the field
|
|
7
|
+
# takes "active" | "passive", not list three requests.
|
|
8
|
+
class Schema
|
|
9
|
+
# Beyond this many distinct values a field is an open set, not a choice.
|
|
10
|
+
ENUM_LIMIT = 6
|
|
11
|
+
|
|
12
|
+
Field = Struct.new(:path, :types, :values, :present_count, :total_count, :repetition, keyword_init: true) do
|
|
13
|
+
def type
|
|
14
|
+
types.to_a.sort.join(" | ")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A field missing from a working request cannot be required.
|
|
18
|
+
def required?
|
|
19
|
+
present_count == total_count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Telling a closed set from free text: either a value came back in more
|
|
23
|
+
# than one request, or every value reads like a token ("active"), not
|
|
24
|
+
# like content ("Ada Lovelace", "ada@example.com").
|
|
25
|
+
def enum?
|
|
26
|
+
return false unless values.size.between?(2, ENUM_LIMIT)
|
|
27
|
+
return false unless values.all? { |value| scalar?(value) }
|
|
28
|
+
return false if identifier?
|
|
29
|
+
|
|
30
|
+
# Repetition only counts among the requests that carried the field:
|
|
31
|
+
# two requests, two different values, is not a set.
|
|
32
|
+
return true if repetition && values.size < present_count
|
|
33
|
+
|
|
34
|
+
values.all? { |value| token?(value) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def example
|
|
38
|
+
values.first
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Test order is not stable, so a listed set must be ordered by something
|
|
42
|
+
# that is.
|
|
43
|
+
def listed_values
|
|
44
|
+
values.sort_by { |value| [value.to_s, value.class.name] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
{
|
|
49
|
+
path: path,
|
|
50
|
+
type: type,
|
|
51
|
+
required: required?,
|
|
52
|
+
values: listed_values
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scalar?(value)
|
|
59
|
+
value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Every record has a different one, so listing the ones a test happened
|
|
63
|
+
# to use would read as if those were the only allowed values.
|
|
64
|
+
def identifier?
|
|
65
|
+
path.to_s.split(/[.\[\]]/).last.to_s.match?(/\A(id|uuid|.*_id|.*_uuid|.*_key|token|slug)\z/i)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A word, not content: "active", "pending_review", "USD". Digits and
|
|
69
|
+
# dashes usually mean a generated value ("e-00056197", "safari-859f04").
|
|
70
|
+
def token?(value)
|
|
71
|
+
case value
|
|
72
|
+
when true, false then true
|
|
73
|
+
when String then value.match?(/\A[a-z][a-z_]*\z/) || value.match?(/\A[A-Z_]{2,}\z/)
|
|
74
|
+
else false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `payloads` are request bodies, query hashes or path parameter hashes from
|
|
80
|
+
# successful captures. With `repetition: true` a value seen in more than
|
|
81
|
+
# one payload marks a closed set — right for what tests *send*, wrong for
|
|
82
|
+
# what an application *returns*, where fixtures repeat by nature; response
|
|
83
|
+
# bodies are inferred with `repetition: false`, so only token-like values
|
|
84
|
+
# ("open", "done") are listed as a set.
|
|
85
|
+
def self.infer(payloads, repetition: true)
|
|
86
|
+
new(payloads, repetition: repetition).call
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Empty payloads are kept: a request that was accepted without any
|
|
90
|
+
# parameters is the proof that every parameter is optional.
|
|
91
|
+
def initialize(payloads, repetition: true)
|
|
92
|
+
@payloads = Array(payloads)
|
|
93
|
+
@repetition = repetition
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def call
|
|
97
|
+
return self.class.empty if @payloads.empty?
|
|
98
|
+
|
|
99
|
+
fields = {}
|
|
100
|
+
|
|
101
|
+
@payloads.each do |payload|
|
|
102
|
+
flatten(payload).each do |path, value|
|
|
103
|
+
field = fields[path] ||= Field.new(
|
|
104
|
+
path: path,
|
|
105
|
+
types: [],
|
|
106
|
+
values: [],
|
|
107
|
+
present_count: 0,
|
|
108
|
+
total_count: @payloads.size,
|
|
109
|
+
repetition: @repetition
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
field.types |= [type_of(value)]
|
|
113
|
+
field.values |= [value] unless value.nil? || value.is_a?(Hash) || value.is_a?(Array)
|
|
114
|
+
field.present_count += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Result.new(fields.values)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.empty
|
|
122
|
+
Result.new([])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The inferred description of one payload shape.
|
|
126
|
+
class Result
|
|
127
|
+
include Enumerable
|
|
128
|
+
|
|
129
|
+
attr_reader :fields
|
|
130
|
+
|
|
131
|
+
def initialize(fields)
|
|
132
|
+
@fields = fields
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each(&block)
|
|
136
|
+
fields.each(&block)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def empty?
|
|
140
|
+
fields.empty?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def size
|
|
144
|
+
fields.size
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def [](path)
|
|
148
|
+
fields.find { |field| field.path == path }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_a
|
|
152
|
+
fields.map(&:to_h)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
# { "customer" => { "tags" => ["a"] } } -> { "customer.tags[]" => "a" }
|
|
159
|
+
def flatten(value, prefix = nil, result = {})
|
|
160
|
+
case value
|
|
161
|
+
when Hash
|
|
162
|
+
value.each { |key, nested| flatten(nested, prefix ? "#{prefix}.#{key}" : key.to_s, result) }
|
|
163
|
+
when Array
|
|
164
|
+
# An empty array still tells the reader the field is a list.
|
|
165
|
+
result[prefix] = [] if value.empty? && prefix
|
|
166
|
+
value.each { |item| flatten(item, "#{prefix}[]", result) }
|
|
167
|
+
else
|
|
168
|
+
result[prefix] = value if prefix
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
result
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def type_of(value)
|
|
175
|
+
case value
|
|
176
|
+
when String then "string"
|
|
177
|
+
when Integer then "integer"
|
|
178
|
+
when Float then "number"
|
|
179
|
+
when true, false then "boolean"
|
|
180
|
+
when nil then "null"
|
|
181
|
+
when Array then "array"
|
|
182
|
+
when Hash then "object"
|
|
183
|
+
else value.class.name.downcase
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
# Minimal inflection helpers. ActiveSupport is used when it is available so
|
|
5
|
+
# that generated titles match the host application's inflections.
|
|
6
|
+
module Support
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def underscore(value)
|
|
10
|
+
string = value.to_s.dup
|
|
11
|
+
return ActiveSupport::Inflector.underscore(string) if defined?(ActiveSupport::Inflector)
|
|
12
|
+
|
|
13
|
+
string.gsub("::", "/")
|
|
14
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
15
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
16
|
+
.tr("-", "_")
|
|
17
|
+
.downcase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def humanize(value)
|
|
21
|
+
underscore(value).tr("_", " ").strip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def titleize(value)
|
|
25
|
+
humanize(value).split(/\s+/).map { |word| word.empty? ? word : word[0].upcase + word[1..] }.join(" ")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def singularize(value)
|
|
29
|
+
string = value.to_s
|
|
30
|
+
return ActiveSupport::Inflector.singularize(string) if defined?(ActiveSupport::Inflector)
|
|
31
|
+
|
|
32
|
+
case string
|
|
33
|
+
when /ies\z/i then string.sub(/ies\z/i, "y")
|
|
34
|
+
when /(ss|sh|ch|x|z)es\z/i then string.sub(/es\z/i, "")
|
|
35
|
+
when /ss\z/i then string
|
|
36
|
+
when /s\z/i then string.sub(/s\z/i, "")
|
|
37
|
+
else string
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pluralize(value)
|
|
42
|
+
string = value.to_s
|
|
43
|
+
return ActiveSupport::Inflector.pluralize(string) if defined?(ActiveSupport::Inflector)
|
|
44
|
+
|
|
45
|
+
case string
|
|
46
|
+
when /(ss|sh|ch|x|z)\z/i then "#{string}es"
|
|
47
|
+
when /[^aeiou]y\z/i then string.sub(/y\z/i, "ies")
|
|
48
|
+
# Already plural, as resource names usually are.
|
|
49
|
+
when /s\z/i then string
|
|
50
|
+
else "#{string}s"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parameterize(value)
|
|
55
|
+
underscore(value).gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/reqcord.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "rack/utils"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "pathname"
|
|
10
|
+
|
|
11
|
+
require_relative "reqcord/version"
|
|
12
|
+
require_relative "reqcord/errors"
|
|
13
|
+
require_relative "reqcord/support"
|
|
14
|
+
require_relative "reqcord/configuration"
|
|
15
|
+
|
|
16
|
+
require_relative "reqcord/request_example"
|
|
17
|
+
require_relative "reqcord/response_example"
|
|
18
|
+
require_relative "reqcord/schema"
|
|
19
|
+
require_relative "reqcord/endpoint"
|
|
20
|
+
require_relative "reqcord/dataset"
|
|
21
|
+
|
|
22
|
+
require_relative "reqcord/route_collector"
|
|
23
|
+
|
|
24
|
+
require_relative "reqcord/capture"
|
|
25
|
+
require_relative "reqcord/capture/test_context"
|
|
26
|
+
require_relative "reqcord/capture/collector"
|
|
27
|
+
|
|
28
|
+
require_relative "reqcord/sanitizers/sanitizer"
|
|
29
|
+
require_relative "reqcord/renderers/payload"
|
|
30
|
+
require_relative "reqcord/renderers/curl"
|
|
31
|
+
require_relative "reqcord/exporters"
|
|
32
|
+
require_relative "reqcord/exporters/curl"
|
|
33
|
+
require_relative "reqcord/exporters/markdown"
|
|
34
|
+
require_relative "reqcord/exporters/postman"
|
|
35
|
+
require_relative "reqcord/generator"
|
|
36
|
+
|
|
37
|
+
require_relative "reqcord/railtie" if defined?(Rails::Railtie)
|
|
38
|
+
|
|
39
|
+
module Reqcord
|
|
40
|
+
class << self
|
|
41
|
+
attr_writer :root
|
|
42
|
+
|
|
43
|
+
def root
|
|
44
|
+
return @root if @root
|
|
45
|
+
|
|
46
|
+
if defined?(Rails) &&
|
|
47
|
+
Rails.respond_to?(:root) &&
|
|
48
|
+
Rails.root
|
|
49
|
+
|
|
50
|
+
Rails.root
|
|
51
|
+
else
|
|
52
|
+
Pathname(Dir.pwd)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def configuration
|
|
57
|
+
@configuration ||=
|
|
58
|
+
Configuration.load(
|
|
59
|
+
root: root
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reload_configuration!
|
|
64
|
+
@configuration =
|
|
65
|
+
Configuration.load(
|
|
66
|
+
root: root
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def log(message)
|
|
71
|
+
$stdout.puts("[reqcord] #{message}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def warn(message)
|
|
75
|
+
$stderr.puts("[reqcord] #{message}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|