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,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Exporters
|
|
5
|
+
class Markdown
|
|
6
|
+
def self.call(dataset:, output_dir:, configuration:)
|
|
7
|
+
new(
|
|
8
|
+
dataset: dataset,
|
|
9
|
+
output_dir: output_dir,
|
|
10
|
+
configuration: configuration
|
|
11
|
+
).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(dataset:, output_dir:, configuration:)
|
|
15
|
+
@dataset = dataset
|
|
16
|
+
@output_dir = Pathname(output_dir)
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns every written path.
|
|
21
|
+
def call
|
|
22
|
+
FileUtils.mkdir_p(output_dir)
|
|
23
|
+
|
|
24
|
+
written = [write(output_dir.join("README.md"), index_document)]
|
|
25
|
+
|
|
26
|
+
written_resources.each do |resource|
|
|
27
|
+
directory = output_dir.join(resource.slug)
|
|
28
|
+
|
|
29
|
+
FileUtils.mkdir_p(directory)
|
|
30
|
+
|
|
31
|
+
written << write(
|
|
32
|
+
directory.join("index.md"),
|
|
33
|
+
resource_document(resource)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
filenames(resource).each do |endpoint, filename|
|
|
37
|
+
next unless documented?(endpoint)
|
|
38
|
+
|
|
39
|
+
written << write(
|
|
40
|
+
directory.join(filename),
|
|
41
|
+
endpoint_document(resource, endpoint)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
written
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :dataset, :output_dir, :configuration
|
|
52
|
+
|
|
53
|
+
# An endpoint gets a page when a test reached it, or when the project
|
|
54
|
+
# asked for the gaps to be written out too.
|
|
55
|
+
def documented?(endpoint)
|
|
56
|
+
endpoint.curl_ready? || configuration.include_uncovered?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A resource with nothing to show gets no directory, so the index never
|
|
60
|
+
# links to a page that was not written.
|
|
61
|
+
def written_resources
|
|
62
|
+
@written_resources ||= dataset.resources.select do |resource|
|
|
63
|
+
resource.endpoints.any? { |endpoint| documented?(endpoint) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def written?(resource)
|
|
68
|
+
written_resources.include?(resource)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write(path, content)
|
|
72
|
+
File.write(path, content)
|
|
73
|
+
|
|
74
|
+
path.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def filenames(resource)
|
|
78
|
+
resource.file_basenames.transform_values { |basename| "#{basename}.md" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def index_document
|
|
82
|
+
lines = ["# API Documentation", ""]
|
|
83
|
+
lines << "Generated by Reqcord from the application's integration tests."
|
|
84
|
+
lines << ""
|
|
85
|
+
lines << "Base URL: `#{configuration.base_url}`"
|
|
86
|
+
lines << ""
|
|
87
|
+
lines.concat(placeholders_section)
|
|
88
|
+
|
|
89
|
+
dataset.resources.each do |resource|
|
|
90
|
+
lines << if written?(resource)
|
|
91
|
+
"## [#{resource.title}](#{resource.slug}/index.md)"
|
|
92
|
+
else
|
|
93
|
+
"## #{resource.title}"
|
|
94
|
+
end
|
|
95
|
+
lines << ""
|
|
96
|
+
lines.concat(namespace_line(resource))
|
|
97
|
+
lines.concat(endpoint_table(resource, prefix: "#{resource.slug}/"))
|
|
98
|
+
lines << ""
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
undocumented = dataset.endpoints.reject(&:curl_ready?)
|
|
102
|
+
|
|
103
|
+
unless undocumented.empty?
|
|
104
|
+
lines << "## No Successful Request Captured"
|
|
105
|
+
lines << ""
|
|
106
|
+
lines << "No successful 2xx request was captured for these routes:"
|
|
107
|
+
lines << ""
|
|
108
|
+
undocumented.each { |endpoint| lines << "- `#{endpoint.method} #{endpoint.path}`" }
|
|
109
|
+
lines << ""
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
lines.join("\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Sanitized values are written as placeholders; the reader has to know
|
|
116
|
+
# what to substitute.
|
|
117
|
+
def placeholders_section
|
|
118
|
+
placeholders = configuration.sanitized_headers.values.grep(/\{\{.+\}\}/)
|
|
119
|
+
|
|
120
|
+
return [] if placeholders.empty?
|
|
121
|
+
|
|
122
|
+
rows = configuration.sanitized_headers.filter_map do |header, value|
|
|
123
|
+
"| #{header} | `#{value}` |" if value.to_s.match?(/\{\{.+\}\}/)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
["## Placeholders", "", "| Header | Replace with |", "| --- | --- |", *rows, ""]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Two resources can share a title (admin/customers, api/v2/customers);
|
|
130
|
+
# the namespace tells them apart.
|
|
131
|
+
def namespace_line(resource)
|
|
132
|
+
return [] if resource.namespace.empty?
|
|
133
|
+
|
|
134
|
+
["Namespace: `#{resource.namespace}`", ""]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def resource_document(resource)
|
|
138
|
+
lines = ["# #{resource.title}", ""]
|
|
139
|
+
lines.concat(namespace_line(resource))
|
|
140
|
+
|
|
141
|
+
versions = resource.api_versions
|
|
142
|
+
|
|
143
|
+
unless versions.empty?
|
|
144
|
+
lines << "API version: #{versions.map { |version| "`#{version}`" }.join(', ')}"
|
|
145
|
+
lines << ""
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
lines.concat(endpoint_table(resource))
|
|
149
|
+
lines << ""
|
|
150
|
+
lines.join("\n")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def endpoint_table(resource, prefix: "")
|
|
154
|
+
rows = filenames(resource).map do |endpoint, filename|
|
|
155
|
+
path =
|
|
156
|
+
if documented?(endpoint)
|
|
157
|
+
"[`#{endpoint.path}`](#{prefix}#{filename})"
|
|
158
|
+
else
|
|
159
|
+
"`#{endpoint.path}`"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
covered = endpoint.curl_ready? ? "" : " _(no successful request captured)_"
|
|
163
|
+
|
|
164
|
+
"| `#{endpoint.method}` | #{path} | #{endpoint.name}#{covered} |"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
["| Method | Path | Description |", "| --- | --- | --- |", *rows]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def endpoint_document(resource, endpoint)
|
|
171
|
+
example = endpoint.primary_request_example
|
|
172
|
+
|
|
173
|
+
lines = ["# #{endpoint.name}", "", endpoint_line(endpoint), ""]
|
|
174
|
+
lines.concat(namespace_line(resource))
|
|
175
|
+
|
|
176
|
+
unless endpoint.curl_ready?
|
|
177
|
+
lines << "_No successful 2xx request was captured for this endpoint yet._"
|
|
178
|
+
lines << ""
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
lines.concat(headers_section(example))
|
|
182
|
+
lines.concat(schema_section("Path Parameters", endpoint.path_param_schema))
|
|
183
|
+
lines.concat(schema_section("Query Parameters", endpoint.query_schema))
|
|
184
|
+
lines.concat(schema_section("Body Parameters", endpoint.body_schema))
|
|
185
|
+
lines.concat(request_body_section(example))
|
|
186
|
+
lines.concat(curl_section(example))
|
|
187
|
+
lines.concat(responses_section(endpoint))
|
|
188
|
+
lines.concat(["---", "", "Resource: [#{resource.title}](index.md)", ""])
|
|
189
|
+
|
|
190
|
+
lines.join("\n")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Field, type, whether every accepted request carried it, and the values
|
|
194
|
+
# those requests used. Values are listed only when they look like a
|
|
195
|
+
# closed set; otherwise one of them stands as an example.
|
|
196
|
+
def schema_section(title, schema, level: 2)
|
|
197
|
+
return [] if schema.empty?
|
|
198
|
+
|
|
199
|
+
rows = schema.map do |field|
|
|
200
|
+
"| `#{field.path}` | #{field.type} | #{field.required? ? 'yes' : 'no'} | #{values_cell(field)} |"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
["#{'#' * level} #{title}", "", "| Field | Type | Required | Values |", "| --- | --- | --- | --- |", *rows, ""]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def values_cell(field)
|
|
207
|
+
return "-" if field.values.empty?
|
|
208
|
+
return field.listed_values.map { |value| "`#{value.inspect}`" }.join(" \\| ") if field.enum?
|
|
209
|
+
|
|
210
|
+
"`#{field.example.inspect}`"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# `PATCH /api/cart` (also `PUT`)
|
|
214
|
+
def endpoint_line(endpoint)
|
|
215
|
+
line = "`#{endpoint.method} #{endpoint.path}`"
|
|
216
|
+
|
|
217
|
+
return line if endpoint.also_methods.empty?
|
|
218
|
+
|
|
219
|
+
"#{line} (also #{endpoint.also_methods.map { |method| "`#{method}`" }.join(', ')})"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def headers_section(example)
|
|
223
|
+
return [] if example.nil? || example.headers.empty?
|
|
224
|
+
|
|
225
|
+
rows = example.headers.map { |name, value| "| #{name} | `#{value}` |" }
|
|
226
|
+
|
|
227
|
+
["## Headers", "", "| Header | Value |", "| --- | --- |", *rows, ""]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A form body is shown as the key/value structure the test built, which
|
|
231
|
+
# reads better than the encoded string; the note says how it travels.
|
|
232
|
+
def request_body_section(example)
|
|
233
|
+
return [] unless example&.body?
|
|
234
|
+
|
|
235
|
+
lines = ["## Example Request", ""]
|
|
236
|
+
|
|
237
|
+
unless Renderers::Payload.json?(example) || example.content_type.to_s.empty?
|
|
238
|
+
lines << "Sent as `#{example.content_type}`; the cURL below carries it in that encoding."
|
|
239
|
+
lines << ""
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
lines.concat([*code_block(example.body, example.content_type), ""])
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def curl_section(example)
|
|
246
|
+
return [] if example.nil?
|
|
247
|
+
|
|
248
|
+
["## cURL", "", "```bash", curl_for(example), "```", ""]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def responses_section(endpoint)
|
|
252
|
+
return [] if endpoint.response_examples.empty?
|
|
253
|
+
|
|
254
|
+
lines = ["## Responses", ""]
|
|
255
|
+
|
|
256
|
+
endpoint.responses.each do |response|
|
|
257
|
+
lines << "### #{response.example.title}"
|
|
258
|
+
lines << ""
|
|
259
|
+
lines.concat(schema_section("Fields", response.schema, level: 4))
|
|
260
|
+
lines.concat(response_body(response.example))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
lines
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def response_body(example)
|
|
267
|
+
return ["_No response body._", ""] unless example.body?
|
|
268
|
+
|
|
269
|
+
[*code_block(example.body, example.content_type), ""]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def curl_for(example)
|
|
273
|
+
Renderers::Curl.call(
|
|
274
|
+
example,
|
|
275
|
+
base_url: configuration.base_url
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def code_block(value, content_type)
|
|
280
|
+
case value
|
|
281
|
+
when Hash, Array then ["```json", pretty_json(value), "```"]
|
|
282
|
+
else ["```#{content_type.to_s.include?('json') ? 'json' : 'text'}", value.to_s, "```"]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def pretty_json(value)
|
|
287
|
+
JSON.pretty_generate(value)
|
|
288
|
+
rescue JSON::GeneratorError
|
|
289
|
+
value.to_s
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
register("markdown", Markdown)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
module Exporters
|
|
5
|
+
# Writes a Postman Collection v2.1 (`postman/collection.json`). Folders
|
|
6
|
+
# follow the controller namespaces, each documented endpoint is one request
|
|
7
|
+
# built from the successful captured example, and every captured status
|
|
8
|
+
# is saved as a response example. Hoppscotch imports the same file.
|
|
9
|
+
class Postman
|
|
10
|
+
SCHEMA_URL = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
11
|
+
PLACEHOLDER = /\{\{([A-Za-z0-9_]+)\}\}/
|
|
12
|
+
BEARER = /\ABearer\s+\{\{([A-Za-z0-9_]+)\}\}\z/i
|
|
13
|
+
|
|
14
|
+
def self.call(dataset:, output_dir:, configuration:)
|
|
15
|
+
new(
|
|
16
|
+
dataset: dataset,
|
|
17
|
+
output_dir: output_dir,
|
|
18
|
+
configuration: configuration
|
|
19
|
+
).call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(dataset:, output_dir:, configuration:)
|
|
23
|
+
@dataset = dataset
|
|
24
|
+
@output_dir = Pathname(output_dir).join("postman")
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
FileUtils.mkdir_p(output_dir)
|
|
30
|
+
|
|
31
|
+
path = output_dir.join("collection.json")
|
|
32
|
+
File.write(path, "#{JSON.pretty_generate(collection)}\n")
|
|
33
|
+
|
|
34
|
+
[path.to_s]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def collection
|
|
38
|
+
{
|
|
39
|
+
info: {
|
|
40
|
+
name: "#{File.basename(configuration.root.to_s)} API",
|
|
41
|
+
description: "Generated by Reqcord from the application's integration tests.",
|
|
42
|
+
schema: SCHEMA_URL
|
|
43
|
+
},
|
|
44
|
+
item: folders,
|
|
45
|
+
variable: variables,
|
|
46
|
+
auth: auth
|
|
47
|
+
}.compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :dataset, :output_dir, :configuration
|
|
53
|
+
|
|
54
|
+
# api/v2/customers -> Api > V2 > Customers, requests in the leaf folder.
|
|
55
|
+
def folders
|
|
56
|
+
tree = { items: [], children: {} }
|
|
57
|
+
|
|
58
|
+
dataset.resources.each do |resource|
|
|
59
|
+
requests = resource.endpoints.select(&:curl_ready?).map { |endpoint| request_item(endpoint) }
|
|
60
|
+
next if requests.empty?
|
|
61
|
+
|
|
62
|
+
leaf = resource.segments.reduce(tree) do |node, segment|
|
|
63
|
+
node[:children][segment] ||= { items: [], children: {} }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
leaf[:items].concat(requests)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
render_folders(tree)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_folders(node)
|
|
73
|
+
node[:children].map do |segment, child|
|
|
74
|
+
{ name: Support.titleize(segment), item: render_folders(child) + child[:items] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def request_item(endpoint)
|
|
79
|
+
{
|
|
80
|
+
name: endpoint.name,
|
|
81
|
+
request: request_object(endpoint.primary_request_example, endpoint),
|
|
82
|
+
response: endpoint.responses.map { |response| response_object(response, endpoint) }
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def request_object(example, endpoint)
|
|
87
|
+
{
|
|
88
|
+
method: example.method,
|
|
89
|
+
description: description(endpoint),
|
|
90
|
+
header: headers(example),
|
|
91
|
+
url: url_object(example),
|
|
92
|
+
body: body_object(example),
|
|
93
|
+
auth: request_auth(example)
|
|
94
|
+
}.compact
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def description(endpoint)
|
|
98
|
+
lines = ["#{endpoint.method} #{endpoint.path}", "#{endpoint.controller}##{endpoint.action}"]
|
|
99
|
+
lines << "Also answers #{endpoint.also_methods.join(', ')}." unless endpoint.also_methods.empty?
|
|
100
|
+
lines << "Route: #{endpoint.route_name}" if endpoint.route_name
|
|
101
|
+
|
|
102
|
+
lines.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The bearer credential lives on the collection; a request that carried
|
|
106
|
+
# it inherits, one that did not says so, so public endpoints replay
|
|
107
|
+
# exactly as their tests did.
|
|
108
|
+
def headers(example)
|
|
109
|
+
example.headers.filter_map do |key, value|
|
|
110
|
+
next if auth && key.to_s.casecmp?("Authorization")
|
|
111
|
+
|
|
112
|
+
{ key: key.to_s, value: value.to_s }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def request_auth(example)
|
|
117
|
+
return nil unless auth
|
|
118
|
+
return nil if example.headers.any? { |key, _| key.to_s.casecmp?("Authorization") }
|
|
119
|
+
|
|
120
|
+
{ type: "noauth" }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def url_object(example)
|
|
124
|
+
url = {
|
|
125
|
+
raw: "{{base_url}}#{Renderers::Payload.path_with_query(example)}",
|
|
126
|
+
host: ["{{base_url}}"],
|
|
127
|
+
path: example.path.split("/").reject(&:empty?),
|
|
128
|
+
query: Renderers::Payload.query_pairs(example).map { |key, value| { key: key, value: value.to_s } }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
url.delete(:query) if url[:query].empty?
|
|
132
|
+
url
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def body_object(example)
|
|
136
|
+
return nil unless example.body?
|
|
137
|
+
|
|
138
|
+
if Renderers::Payload.json?(example)
|
|
139
|
+
{ mode: "raw", raw: Renderers::Payload.raw_body(example), options: { raw: { language: "json" } } }
|
|
140
|
+
elsif example.body.is_a?(Hash)
|
|
141
|
+
pairs = Renderers::Payload.form_pairs(example).map { |key, value| { key: key, value: value.to_s, type: "text" } }
|
|
142
|
+
|
|
143
|
+
{ mode: "urlencoded", urlencoded: pairs }
|
|
144
|
+
else
|
|
145
|
+
{ mode: "raw", raw: example.body.to_s }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def response_object(response, endpoint)
|
|
150
|
+
example = response.example
|
|
151
|
+
request = endpoint.request_examples.find { |candidate| candidate.response_status == response.status } ||
|
|
152
|
+
endpoint.primary_request_example
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
name: example.title,
|
|
156
|
+
originalRequest: request_object(request, endpoint),
|
|
157
|
+
status: example.status_text,
|
|
158
|
+
code: response.status,
|
|
159
|
+
_postman_previewlanguage: example.content_type.to_s.include?("json") ? "json" : "text",
|
|
160
|
+
header: example.headers.map { |key, value| { key: key.to_s, value: value.to_s } },
|
|
161
|
+
body: response_body(example)
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def response_body(example)
|
|
166
|
+
return "" unless example.body?
|
|
167
|
+
return example.body if example.body.is_a?(String)
|
|
168
|
+
|
|
169
|
+
JSON.pretty_generate(example.body)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# `base_url`, plus every `{{name}}` the sanitizer wrote into the
|
|
173
|
+
# documented examples, so the collection declares what it expects.
|
|
174
|
+
def variables
|
|
175
|
+
names = documented_examples.flat_map do |example|
|
|
176
|
+
[example.headers, example.body, example.query_params].flat_map do |value|
|
|
177
|
+
JSON.generate(value).scan(PLACEHOLDER).flatten
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
names |= (configuration.sanitized_headers.values + configuration.sanitized_body_keys.values)
|
|
182
|
+
.flat_map { |value| value.to_s.scan(PLACEHOLDER).flatten }
|
|
183
|
+
|
|
184
|
+
[{ key: "base_url", value: configuration.base_url, type: "string" }] +
|
|
185
|
+
(names - ["base_url"]).uniq.sort.map { |name| { key: name, value: "", type: "string" } }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def auth
|
|
189
|
+
return @auth if defined?(@auth)
|
|
190
|
+
|
|
191
|
+
placeholder = documented_examples.filter_map do |example|
|
|
192
|
+
value = example.headers.find { |key, _| key.to_s.casecmp?("Authorization") }&.last
|
|
193
|
+
value.to_s[BEARER, 1]
|
|
194
|
+
end.first
|
|
195
|
+
|
|
196
|
+
@auth = placeholder && { type: "bearer", bearer: [{ key: "token", value: "{{#{placeholder}}}", type: "string" }] }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def documented_examples
|
|
200
|
+
@documented_examples ||= dataset.curl_ready_endpoints.flat_map(&:request_examples)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
register("postman", Postman)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reqcord
|
|
4
|
+
# Exporters register themselves here, so adding one means adding a file
|
|
5
|
+
# rather than editing the pipeline.
|
|
6
|
+
module Exporters
|
|
7
|
+
class << self
|
|
8
|
+
def register(name, exporter)
|
|
9
|
+
registry[name.to_s] = exporter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(name)
|
|
13
|
+
registry.fetch(name.to_s) do
|
|
14
|
+
raise ConfigurationError,
|
|
15
|
+
"unknown exporter #{name.inspect}, expected one of #{names.join(', ')}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def registered?(name)
|
|
20
|
+
registry.key?(name.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def names
|
|
24
|
+
registry.keys.sort
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def registry
|
|
28
|
+
@registry ||= {}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|