pact_broker 2.121.2 → 2.122.0
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/CHANGELOG.md +73 -0
- data/Gemfile +1 -4
- data/lib/pact_broker/api/renderers/html_pact_renderer.rb +6 -69
- data/lib/pact_broker/api/renderers/markdown/consumer_contract_renderer.rb +87 -0
- data/lib/pact_broker/api/renderers/markdown/interaction_renderer.rb +52 -0
- data/lib/pact_broker/api/renderers/markdown/interaction_view_model.rb +162 -0
- data/lib/pact_broker/api/renderers/markdown.rb +9 -0
- data/lib/pact_broker/pacts/create_formatted_diff.rb +2 -7
- data/lib/pact_broker/pacts/repository.rb +1 -2
- data/lib/pact_broker/unified_diff.rb +26 -0
- data/lib/pact_broker/version.rb +1 -1
- data/pact_broker.gemspec +1 -1
- metadata +23 -28
- data/lib/pact/doc/doc_file.rb +0 -38
- data/lib/pact/doc/generate.rb +0 -11
- data/lib/pact/doc/generator.rb +0 -82
- data/lib/pact/doc/interaction_view_model.rb +0 -157
- data/lib/pact/doc/markdown/consumer_contract_renderer.rb +0 -74
- data/lib/pact/doc/markdown/generator.rb +0 -24
- data/lib/pact/doc/markdown/index_renderer.rb +0 -40
- data/lib/pact/doc/markdown/interaction_renderer.rb +0 -51
- data/lib/pact/doc/sort_interactions.rb +0 -14
- data/lib/pact/doc.rb +0 -5
- /data/lib/{pact/doc → pact_broker/api/renderers}/markdown/interaction.erb +0 -0
- /data/lib/{pact/doc → pact_broker/api/renderers}/markdown/interaction_async.erb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f515fab91233eb259d233ef8ff18b35014c55dfebb10a5cec183283bbe760d54
|
|
4
|
+
data.tar.gz: 2f35f5f3f748d3df875df0ce5b8270f2b1e4b2cf8682804fda41def6d31b88a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eaefdf8cd5b2b9856bb076579bf034889b7fdab56cfe159c6e1ed56e74d72dca7929ca983290238e32f280e8d7e9b9bc53b9512b4d1562bde122de035e2f50b
|
|
7
|
+
data.tar.gz: fe3c3008fa385b8585d7b2faa08e8196194e92fdfc496a813f0208373905abacdcded76e0eb4b8dc115b588b20be7e5cd32580ec78c4cbddfc7db15222936b73
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
## [2.122.0] _2026-10-01_
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- Add a unified diff renderer
|
|
6
|
+
- Render pact diffs as unified diffs
|
|
7
|
+
> **Migration note:** the pact diff endpoints return a standard unified diff.
|
|
8
|
+
> This covers the `diff/previous-distinct`, `diff/version/{version}` and
|
|
9
|
+
> `pact-version/{sha}/diff/pact-version/{sha}` resources under
|
|
10
|
+
> `/pacts/provider/{provider}/consumer/{consumer}`, and the diff notice
|
|
11
|
+
> that `POST /contracts/publish` returns when a publish would modify an
|
|
12
|
+
> existing pact. Clients that parse this output must handle `@@` hunk
|
|
13
|
+
> headers and unchanged context lines. Keys that exist only in the newer
|
|
14
|
+
> pact now appear as `+` lines.
|
|
15
|
+
>
|
|
16
|
+
> As an example, take an interaction whose request method changes from
|
|
17
|
+
> post to get and which gains an `Accept` header. The diff was previously:
|
|
18
|
+
>
|
|
19
|
+
> ```diff
|
|
20
|
+
> "interactions": [
|
|
21
|
+
> {
|
|
22
|
+
> "request": {
|
|
23
|
+
> - "method": "post"
|
|
24
|
+
> + "method": "get"
|
|
25
|
+
> }
|
|
26
|
+
> }
|
|
27
|
+
> ]
|
|
28
|
+
> ```
|
|
29
|
+
>
|
|
30
|
+
> It is now:
|
|
31
|
+
>
|
|
32
|
+
> ```diff
|
|
33
|
+
> @@ -6,7 +6,10 @@
|
|
34
|
+
> {
|
|
35
|
+
> "description": "a request",
|
|
36
|
+
> "request": {
|
|
37
|
+
> - "method": "post",
|
|
38
|
+
> + "headers": {
|
|
39
|
+
> + "Accept": "application/json"
|
|
40
|
+
> + },
|
|
41
|
+
> + "method": "get",
|
|
42
|
+
> "path": "/"
|
|
43
|
+
> },
|
|
44
|
+
> "response": {
|
|
45
|
+
> ```
|
|
46
|
+
|
|
47
|
+
### 🐛 Bug Fixes
|
|
48
|
+
|
|
49
|
+
- Require date_helper in HtmlPactRenderer
|
|
50
|
+
|
|
51
|
+
### 🚜 Refactor
|
|
52
|
+
|
|
53
|
+
- Remove unused pact/doc generator code
|
|
54
|
+
- Compare pact content with Hash equality
|
|
55
|
+
- Move lib/pact/doc under lib/pact_broker/api/renderers
|
|
56
|
+
- Move the markdown pact renderer under PactBroker
|
|
57
|
+
- Render pact HTML from the parsed hash
|
|
58
|
+
|
|
59
|
+
### 🧪 Testing
|
|
60
|
+
|
|
61
|
+
- Stop snapshotting non-determinate response headers
|
|
62
|
+
- Replace Pact::Matchers in spec helpers with a subset matcher
|
|
63
|
+
|
|
64
|
+
### 🛠️ Miscellaneous Tasks
|
|
65
|
+
|
|
66
|
+
- _(ci)_ Standardize renovate config
|
|
67
|
+
- _(ci)_ Remove stale internal workflow
|
|
68
|
+
- Remove the pact-support dependency
|
|
69
|
+
> **Migration note:** pact_broker no longer depends on pact-support, so code
|
|
70
|
+
> alongside the broker can no longer rely on Pact::* constants. Add
|
|
71
|
+
> pact-support to your own Gemfile if that code needs them.
|
|
72
|
+
- _(release)_ Render Migration-Note footers in the changelog
|
|
73
|
+
|
|
1
74
|
## [2.121.2] _2026-09-17_
|
|
2
75
|
|
|
3
76
|
### 🐛 Bug Fixes
|
data/Gemfile
CHANGED
|
@@ -26,7 +26,7 @@ group :test do
|
|
|
26
26
|
gem "pact-ffi", path: "../pact-ruby-ffi"
|
|
27
27
|
else
|
|
28
28
|
gem "pact", ">=2.0.3"
|
|
29
|
-
gem "pact-ffi", "~>0.
|
|
29
|
+
gem "pact-ffi", "~>0.5.0"
|
|
30
30
|
end
|
|
31
31
|
gem "rack-test", ">= 0.6.3", "< 3.0.0"
|
|
32
32
|
gem "bundler-audit", "~>0.4"
|
|
@@ -47,6 +47,3 @@ group :pg, optional: true do
|
|
|
47
47
|
gem "pg", "~>1.6"
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
if ENV["X_PACT_DEVELOPMENT"] == "true"
|
|
51
|
-
gem "pact-support", path: "../pact-support"
|
|
52
|
-
end
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
require "pact/consumer_contract"
|
|
2
|
-
require "pact/reification"
|
|
3
1
|
require "redcarpet"
|
|
4
|
-
require "
|
|
2
|
+
require "pact_broker/api/renderers/markdown/consumer_contract_renderer"
|
|
5
3
|
require "pact_broker/api/pact_broker_urls"
|
|
6
4
|
require "pact_broker/logging"
|
|
5
|
+
require "pact_broker/date_helper"
|
|
7
6
|
require "rack"
|
|
8
7
|
|
|
9
8
|
module PactBroker
|
|
@@ -11,8 +10,6 @@ module PactBroker
|
|
|
11
10
|
module Renderers
|
|
12
11
|
class HtmlPactRenderer
|
|
13
12
|
|
|
14
|
-
class NotAPactError < StandardError; end
|
|
15
|
-
|
|
16
13
|
include PactBroker::Logging
|
|
17
14
|
|
|
18
15
|
def self.call pact, options = {}
|
|
@@ -182,10 +179,11 @@ module PactBroker
|
|
|
182
179
|
end
|
|
183
180
|
|
|
184
181
|
def markdown
|
|
185
|
-
|
|
186
|
-
rescue StandardError
|
|
182
|
+
Markdown::ConsumerContractRenderer.call(JSON.parse(@json_content))
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
logger.info "Could not render the following content as a pact due to #{e.class} #{e.message}, showing raw content instead: #{@json_content}"
|
|
187
185
|
heading = "### A contract between #{@pact.consumer.name} and #{@pact.provider.name}"
|
|
188
|
-
warning = "_Note: this
|
|
186
|
+
warning = "_Note: this content could not be rendered as a pact, showing raw JSON instead._"
|
|
189
187
|
pretty_json = JSON.pretty_generate(@pact.content_hash)
|
|
190
188
|
"#{heading}\n#{warning}\n```json\n#{pretty_json}\n```\n"
|
|
191
189
|
end
|
|
@@ -194,67 +192,6 @@ module PactBroker
|
|
|
194
192
|
Redcarpet::Markdown.new(Redcarpet::Render::HTML, :fenced_code_blocks => true, :lax_spacing => true).render(markdown)
|
|
195
193
|
end
|
|
196
194
|
|
|
197
|
-
def consumer_contract
|
|
198
|
-
pact_object = JSON.parse(@json_content)
|
|
199
|
-
convert_v3_messages_to_interactions(pact_object)
|
|
200
|
-
|
|
201
|
-
pact_object["interactions"]&.each do |interaction|
|
|
202
|
-
add_dummy_html_request_response_to_asynchronous_message(interaction)
|
|
203
|
-
add_dummy_html_request_response_to_synchronous_message(interaction)
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
new_json_content = pact_object.to_json
|
|
207
|
-
|
|
208
|
-
Pact::ConsumerContract.from_json(new_json_content)
|
|
209
|
-
rescue => e
|
|
210
|
-
logger.info "Could not parse the following content to a Pact due to #{e.class} #{e.message}, showing raw content instead: #{@json_content}"
|
|
211
|
-
raise NotAPactError
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def convert_v3_messages_to_interactions(pact_object)
|
|
215
|
-
return unless pact_object.key?("messages")
|
|
216
|
-
|
|
217
|
-
logger.warn "Detected a v3 Pact, converting 'messages' to 'interactions'."
|
|
218
|
-
pact_object["interactions"] = pact_object.delete("messages")
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def add_dummy_html_request_response_to_asynchronous_message(interaction)
|
|
222
|
-
if !interaction["request"] || !interaction["response"]
|
|
223
|
-
logger.warn "Interaction '#{interaction['description']}' does not have a request or response, adding dummy request and response so that they can be parsed and appear on the UI."
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
interaction["request"] ||= {
|
|
227
|
-
method: "FAKE_ASYNC_METHOD",
|
|
228
|
-
path: interaction["description"]
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
unless interaction.key?("response")
|
|
232
|
-
interaction["response"] = {
|
|
233
|
-
status: "FAKE_ASYNC_METHOD",
|
|
234
|
-
body: {
|
|
235
|
-
contents: interaction.delete("contents"),
|
|
236
|
-
metadata: interaction.delete("metadata")
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def add_dummy_html_request_response_to_synchronous_message(interaction)
|
|
243
|
-
if interaction["type"] == "Synchronous/Messages"
|
|
244
|
-
interaction["request"] = {
|
|
245
|
-
method: "FAKE_SYNC_METHOD",
|
|
246
|
-
path: interaction["description"],
|
|
247
|
-
body: interaction.delete("request"),
|
|
248
|
-
}
|
|
249
|
-
interaction["response"] = {
|
|
250
|
-
status: "FAKE_SYNC_METHOD",
|
|
251
|
-
body: {
|
|
252
|
-
contents: interaction.delete("response"),
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
195
|
def h string
|
|
259
196
|
Rack::Utils.escape_html(string)
|
|
260
197
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "pact_broker/api/renderers/markdown/interaction_renderer"
|
|
2
|
+
require "pact_broker/api/renderers/markdown/interaction_view_model"
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
require "pact_broker/api/renderers/markdown"
|
|
5
|
+
|
|
6
|
+
module PactBroker
|
|
7
|
+
module Api
|
|
8
|
+
module Renderers
|
|
9
|
+
module Markdown
|
|
10
|
+
class ConsumerContractRenderer
|
|
11
|
+
|
|
12
|
+
# Raised when the content does not have the shape of a pact.
|
|
13
|
+
class NotAPactError < StandardError; end
|
|
14
|
+
|
|
15
|
+
def initialize pact_hash
|
|
16
|
+
unless pact_hash.is_a?(Hash) && pact_hash["consumer"].is_a?(Hash) && pact_hash["provider"].is_a?(Hash)
|
|
17
|
+
raise NotAPactError, "content does not have consumer and provider objects"
|
|
18
|
+
end
|
|
19
|
+
@pact_hash = pact_hash
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.call pact_hash
|
|
23
|
+
new(pact_hash).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
title + summaries_title + summaries + interactions_title + full_interactions
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :pact_hash
|
|
33
|
+
|
|
34
|
+
def title
|
|
35
|
+
"# A pact between #{consumer_name} and #{provider_name}\n\n"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def interaction_renderers
|
|
39
|
+
@interaction_renderers ||= sorted_interactions.collect { |view_model| InteractionRenderer.new(view_model) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def summaries_title
|
|
43
|
+
"### Requests from #{consumer_name} to #{provider_name}\n\n"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def interactions_title
|
|
47
|
+
"### Interactions\n\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def summaries
|
|
51
|
+
interaction_renderers.collect(&:render_summary).join
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def full_interactions
|
|
55
|
+
interaction_renderers.collect(&:render_full_interaction).join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sorted_interactions
|
|
59
|
+
interactions.collect { |interaction| InteractionViewModel.new(interaction, pact_hash) }.sort_by(&:sortable_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# v3 message pacts keep their interactions under "messages"
|
|
63
|
+
def interactions
|
|
64
|
+
Array(pact_hash["interactions"] || pact_hash["messages"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def consumer_name
|
|
68
|
+
h(markdown_escape pact_hash.dig("consumer", "name"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def provider_name
|
|
72
|
+
h(markdown_escape pact_hash.dig("provider", "name"))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def markdown_escape string
|
|
76
|
+
return nil unless string
|
|
77
|
+
string.gsub(MARKDOWN_SPECIAL_CHARS_REGEXP) { |char| "\\#{char}" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def h(text)
|
|
81
|
+
Rack::Utils.escape_html(text)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "rack/utils"
|
|
3
|
+
|
|
4
|
+
module PactBroker
|
|
5
|
+
module Api
|
|
6
|
+
module Renderers
|
|
7
|
+
module Markdown
|
|
8
|
+
class InteractionRenderer
|
|
9
|
+
attr_reader :interaction
|
|
10
|
+
|
|
11
|
+
def initialize interaction_view_model
|
|
12
|
+
@interaction = interaction_view_model
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_summary
|
|
16
|
+
formatted_provider_states = h(interaction.formatted_provider_states)
|
|
17
|
+
suffix = formatted_provider_states.empty? ? "" : " given #{formatted_provider_states}"
|
|
18
|
+
"* [#{h(interaction.description(true))}](##{interaction.id})#{suffix}\n\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def render_full_interaction
|
|
22
|
+
if interaction.async?
|
|
23
|
+
render("/interaction_async.erb")
|
|
24
|
+
else
|
|
25
|
+
render("/interaction.erb")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render template_file
|
|
30
|
+
ERB.new(template_string(template_file)).result(binding)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The template file is written with only ASCII range characters, so we
|
|
34
|
+
# can read as UTF-8. But rendered strings must have same encoding as
|
|
35
|
+
# script encoding because it will joined to strings which are produced by
|
|
36
|
+
# string literal.
|
|
37
|
+
def template_string(template_file)
|
|
38
|
+
File.read(template_contents(template_file), external_encoding: Encoding::UTF_8).force_encoding(__ENCODING__)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def template_contents(template_file)
|
|
42
|
+
File.dirname(__FILE__) + template_file
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def h(text)
|
|
46
|
+
Rack::Utils.escape_html(text)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require "json"
|
|
3
|
+
require "pact_broker/api/renderers/markdown"
|
|
4
|
+
|
|
5
|
+
module PactBroker
|
|
6
|
+
module Api
|
|
7
|
+
module Renderers
|
|
8
|
+
module Markdown
|
|
9
|
+
# Presents one interaction from a parsed pact hash for the markdown
|
|
10
|
+
# templates. Bodies are printed as stored, so v1 pacts show their
|
|
11
|
+
# inline json_class matcher objects verbatim.
|
|
12
|
+
class InteractionViewModel
|
|
13
|
+
MARKDOWN_BOLD_CHARACTERS = "**"
|
|
14
|
+
ORDERED_KEYS = ["method", "path", "query", "status", "headers", "body"].freeze
|
|
15
|
+
ASYNC_REQUEST = "ASYNC_REQUEST"
|
|
16
|
+
|
|
17
|
+
def initialize interaction, pact
|
|
18
|
+
@interaction = interaction
|
|
19
|
+
@pact = pact
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def id
|
|
23
|
+
@id ||= begin
|
|
24
|
+
full_desc = if has_provider_state?
|
|
25
|
+
"#{description} given #{provider_state_name}"
|
|
26
|
+
else
|
|
27
|
+
description
|
|
28
|
+
end
|
|
29
|
+
CGI.escapeHTML(full_desc.gsub(/\s+/, "_"))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sortable_id
|
|
34
|
+
"#{(interaction["description"] || "").downcase} #{http_response["status"]} #{(provider_state_name || "").downcase}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def consumer_name
|
|
38
|
+
markdown_escape pact.dig("consumer", "name")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def provider_name
|
|
42
|
+
markdown_escape pact.dig("provider", "name")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def has_provider_state?
|
|
46
|
+
!provider_state_name.nil? && !provider_state_name.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def provider_state start_of_sentence = false
|
|
50
|
+
markdown_escape apply_capitals(provider_state_name.strip, start_of_sentence)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def formatted_provider_states mark_bold: false
|
|
54
|
+
bold_marker = mark_bold ? MARKDOWN_BOLD_CHARACTERS : ""
|
|
55
|
+
|
|
56
|
+
provider_state_names.map do |name|
|
|
57
|
+
"#{bold_marker}#{markdown_escape(apply_capitals(name.strip, false))}#{bold_marker}"
|
|
58
|
+
end.join(" and ")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def description start_of_sentence = false
|
|
62
|
+
return "" unless interaction["description"]
|
|
63
|
+
markdown_escape apply_capitals(interaction["description"].strip, start_of_sentence)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def request
|
|
67
|
+
return ASYNC_REQUEST if async?
|
|
68
|
+
JSON.pretty_generate(clean_request)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def response
|
|
72
|
+
JSON.pretty_generate(clean_response)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def async?
|
|
76
|
+
interaction["type"] == "Asynchronous/Messages" || !interaction.key?("request")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
attr_reader :interaction, :pact
|
|
82
|
+
|
|
83
|
+
def sync?
|
|
84
|
+
interaction["type"] == "Synchronous/Messages"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def http_request
|
|
88
|
+
interaction["request"].is_a?(Hash) ? interaction["request"] : {}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def http_response
|
|
92
|
+
interaction["response"].is_a?(Hash) ? interaction["response"] : {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def clean_request
|
|
96
|
+
if sync?
|
|
97
|
+
http_request.slice("contents", "metadata")
|
|
98
|
+
else
|
|
99
|
+
ordered_clean_hash(http_request).tap do |h|
|
|
100
|
+
h["body"] = http_request["body"] if http_request["body"]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def clean_response
|
|
106
|
+
if sync?
|
|
107
|
+
Array(interaction["response"]).map { |item| item.to_h.slice("contents", "metadata") }
|
|
108
|
+
elsif async?
|
|
109
|
+
{ "contents" => interaction["contents"], "metadata" => interaction["metadata"] }
|
|
110
|
+
else
|
|
111
|
+
ordered_clean_hash(http_response)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Removes empty header and body hashes, as an empty hash means
|
|
116
|
+
# "allow anything" and reads more cleanly when omitted.
|
|
117
|
+
def ordered_clean_hash source
|
|
118
|
+
ORDERED_KEYS.each_with_object({}) do |key, target|
|
|
119
|
+
if source.key? key
|
|
120
|
+
target[key] = source[key] unless value_is_an_empty_hash(source[key])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def value_is_an_empty_hash value
|
|
126
|
+
value.is_a?(Hash) && value.empty?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def provider_state_names
|
|
130
|
+
states = interaction["providerStates"]
|
|
131
|
+
if states.is_a?(Array) && states.any?
|
|
132
|
+
states.map { |state| state.is_a?(Hash) ? state["name"] : state }.compact
|
|
133
|
+
else
|
|
134
|
+
[interaction["providerState"] || interaction["provider_state"]].compact
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def provider_state_name
|
|
139
|
+
provider_state_names.first
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_capitals string, start_of_sentence = false
|
|
143
|
+
start_of_sentence ? capitalize_first_letter(string) : lowercase_first_letter(string)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def capitalize_first_letter string
|
|
147
|
+
string[0].upcase + string[1..-1]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def lowercase_first_letter string
|
|
151
|
+
string[0].downcase + string[1..-1]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def markdown_escape string
|
|
155
|
+
return nil unless string
|
|
156
|
+
string.gsub(MARKDOWN_SPECIAL_CHARS_REGEXP) { |char| "\\#{char}" }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
require "pact/support/matchers"
|
|
2
1
|
require "pact_broker/json"
|
|
3
|
-
require "
|
|
2
|
+
require "pact_broker/unified_diff"
|
|
4
3
|
require "pact_broker/pacts/sort_content"
|
|
5
4
|
require "pact_broker/pacts/content"
|
|
6
5
|
|
|
7
6
|
module PactBroker
|
|
8
7
|
module Pacts
|
|
9
8
|
class CreateFormattedDiff
|
|
10
|
-
extend Pact::Matchers
|
|
11
|
-
|
|
12
9
|
def self.call pact_json_content, previous_pact_json_content, raw: false
|
|
13
10
|
pact_hash = JSON.parse(pact_json_content, **PactBroker::PACT_PARSING_OPTIONS)
|
|
14
11
|
previous_pact_hash = JSON.parse(previous_pact_json_content, **PactBroker::PACT_PARSING_OPTIONS)
|
|
@@ -18,9 +15,7 @@ module PactBroker
|
|
|
18
15
|
previous_pact_hash = SortContent.call(PactBroker::Pacts::Content.from_hash(previous_pact_hash).without_ids.to_hash)
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Pact::Matchers::UnixDiffFormatter.call(difference, colour: false, include_explanation: false)
|
|
18
|
+
PactBroker::UnifiedDiff.call(JSON.pretty_generate(previous_pact_hash), JSON.pretty_generate(pact_hash))
|
|
24
19
|
end
|
|
25
20
|
end
|
|
26
21
|
end
|
|
@@ -3,7 +3,6 @@ require "pact_broker/logging"
|
|
|
3
3
|
require "pact_broker/pacts/generate_sha"
|
|
4
4
|
require "pact_broker/pacts/pact_publication"
|
|
5
5
|
require "pact_broker/pacts/pact_version"
|
|
6
|
-
require "pact/shared/json_differ"
|
|
7
6
|
require "pact_broker/domain"
|
|
8
7
|
require "pact_broker/pacts/parse"
|
|
9
8
|
require "pact_broker/pacts/latest_pact_publication_id_for_consumer_version"
|
|
@@ -386,7 +385,7 @@ module PactBroker
|
|
|
386
385
|
end
|
|
387
386
|
|
|
388
387
|
def different? pact, other_pact
|
|
389
|
-
|
|
388
|
+
pact.content_hash != other_pact.content_hash
|
|
390
389
|
end
|
|
391
390
|
|
|
392
391
|
def find_or_create_pact_version consumer_id, provider_id, pact_version_sha, json_content
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "diff/lcs"
|
|
2
|
+
require "diff/lcs/hunk"
|
|
3
|
+
|
|
4
|
+
module PactBroker
|
|
5
|
+
# Renders the difference between two texts in unified diff format,
|
|
6
|
+
# without the file header lines.
|
|
7
|
+
class UnifiedDiff
|
|
8
|
+
def self.call(old_text, new_text, context: 3)
|
|
9
|
+
old_lines = old_text.split("\n")
|
|
10
|
+
new_lines = new_text.split("\n")
|
|
11
|
+
pieces = Diff::LCS.diff(old_lines, new_lines)
|
|
12
|
+
return "" if pieces.empty?
|
|
13
|
+
|
|
14
|
+
hunks = []
|
|
15
|
+
length_difference = 0
|
|
16
|
+
pieces.each do |piece|
|
|
17
|
+
hunk = Diff::LCS::Hunk.new(old_lines, new_lines, piece, context, length_difference)
|
|
18
|
+
length_difference = hunk.file_length_difference
|
|
19
|
+
hunk.merge(hunks.pop) if context.positive? && hunks.last && hunk.overlaps?(hunks.last)
|
|
20
|
+
hunks << hunk
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
hunks.collect { |hunk| hunk.diff(:unified) }.join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/pact_broker/version.rb
CHANGED
data/pact_broker.gemspec
CHANGED
|
@@ -59,7 +59,6 @@ Gem::Specification.new do |gem|
|
|
|
59
59
|
gem.add_runtime_dependency "semver2", "~> 3.4.2"
|
|
60
60
|
gem.add_runtime_dependency "rack", "~> 3.2"
|
|
61
61
|
gem.add_runtime_dependency "redcarpet", "~> 3.5"
|
|
62
|
-
gem.add_runtime_dependency "pact-support", "~> 2.0"
|
|
63
62
|
gem.add_runtime_dependency "haml", "~>5.0"
|
|
64
63
|
gem.add_runtime_dependency "sucker_punch", "~>3.0"
|
|
65
64
|
gem.add_runtime_dependency "rack-protection", "~> 4.1"
|
|
@@ -70,6 +69,7 @@ Gem::Specification.new do |gem|
|
|
|
70
69
|
gem.add_runtime_dependency "anyway_config", "~> 2.1"
|
|
71
70
|
gem.add_runtime_dependency "request_store", "~> 1.5"
|
|
72
71
|
gem.add_runtime_dependency "moments", "~> 0.2"
|
|
72
|
+
gem.add_runtime_dependency "diff-lcs", "~> 1.5"
|
|
73
73
|
# dropped in ruby 3.4 stdlib
|
|
74
74
|
gem.add_runtime_dependency "mutex_m", "~> 0.3" # until as-notifications -> webmachine requires it
|
|
75
75
|
gem.add_runtime_dependency "csv", "~> 3.0" # lib/pact_broker/api/decorators/relationships_csv_decorator.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pact_broker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.122.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bethany Skurrie
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-
|
|
13
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: json
|
|
@@ -152,20 +152,6 @@ dependencies:
|
|
|
152
152
|
- - "~>"
|
|
153
153
|
- !ruby/object:Gem::Version
|
|
154
154
|
version: '3.5'
|
|
155
|
-
- !ruby/object:Gem::Dependency
|
|
156
|
-
name: pact-support
|
|
157
|
-
requirement: !ruby/object:Gem::Requirement
|
|
158
|
-
requirements:
|
|
159
|
-
- - "~>"
|
|
160
|
-
- !ruby/object:Gem::Version
|
|
161
|
-
version: '2.0'
|
|
162
|
-
type: :runtime
|
|
163
|
-
prerelease: false
|
|
164
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
165
|
-
requirements:
|
|
166
|
-
- - "~>"
|
|
167
|
-
- !ruby/object:Gem::Version
|
|
168
|
-
version: '2.0'
|
|
169
155
|
- !ruby/object:Gem::Dependency
|
|
170
156
|
name: haml
|
|
171
157
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -306,6 +292,20 @@ dependencies:
|
|
|
306
292
|
- - "~>"
|
|
307
293
|
- !ruby/object:Gem::Version
|
|
308
294
|
version: '0.2'
|
|
295
|
+
- !ruby/object:Gem::Dependency
|
|
296
|
+
name: diff-lcs
|
|
297
|
+
requirement: !ruby/object:Gem::Requirement
|
|
298
|
+
requirements:
|
|
299
|
+
- - "~>"
|
|
300
|
+
- !ruby/object:Gem::Version
|
|
301
|
+
version: '1.5'
|
|
302
|
+
type: :runtime
|
|
303
|
+
prerelease: false
|
|
304
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
305
|
+
requirements:
|
|
306
|
+
- - "~>"
|
|
307
|
+
- !ruby/object:Gem::Version
|
|
308
|
+
version: '1.5'
|
|
309
309
|
- !ruby/object:Gem::Dependency
|
|
310
310
|
name: mutex_m
|
|
311
311
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -593,18 +593,6 @@ files:
|
|
|
593
593
|
- docs/developer/matrix.md
|
|
594
594
|
- docs/developer/rack.md
|
|
595
595
|
- docs/matrix-optimisation/README.md
|
|
596
|
-
- lib/pact/doc.rb
|
|
597
|
-
- lib/pact/doc/doc_file.rb
|
|
598
|
-
- lib/pact/doc/generate.rb
|
|
599
|
-
- lib/pact/doc/generator.rb
|
|
600
|
-
- lib/pact/doc/interaction_view_model.rb
|
|
601
|
-
- lib/pact/doc/markdown/consumer_contract_renderer.rb
|
|
602
|
-
- lib/pact/doc/markdown/generator.rb
|
|
603
|
-
- lib/pact/doc/markdown/index_renderer.rb
|
|
604
|
-
- lib/pact/doc/markdown/interaction.erb
|
|
605
|
-
- lib/pact/doc/markdown/interaction_async.erb
|
|
606
|
-
- lib/pact/doc/markdown/interaction_renderer.rb
|
|
607
|
-
- lib/pact/doc/sort_interactions.rb
|
|
608
596
|
- lib/pact_broker.rb
|
|
609
597
|
- lib/pact_broker/api.rb
|
|
610
598
|
- lib/pact_broker/api/authorization/resource_access_policy.rb
|
|
@@ -717,6 +705,12 @@ files:
|
|
|
717
705
|
- lib/pact_broker/api/paths.rb
|
|
718
706
|
- lib/pact_broker/api/renderers/html_pact_renderer.rb
|
|
719
707
|
- lib/pact_broker/api/renderers/integrations_dot_renderer.rb
|
|
708
|
+
- lib/pact_broker/api/renderers/markdown.rb
|
|
709
|
+
- lib/pact_broker/api/renderers/markdown/consumer_contract_renderer.rb
|
|
710
|
+
- lib/pact_broker/api/renderers/markdown/interaction.erb
|
|
711
|
+
- lib/pact_broker/api/renderers/markdown/interaction_async.erb
|
|
712
|
+
- lib/pact_broker/api/renderers/markdown/interaction_renderer.rb
|
|
713
|
+
- lib/pact_broker/api/renderers/markdown/interaction_view_model.rb
|
|
720
714
|
- lib/pact_broker/api/resources.rb
|
|
721
715
|
- lib/pact_broker/api/resources/after_reply.rb
|
|
722
716
|
- lib/pact_broker/api/resources/all_webhooks.rb
|
|
@@ -1093,6 +1087,7 @@ files:
|
|
|
1093
1087
|
- lib/pact_broker/ui/views/index/show.haml
|
|
1094
1088
|
- lib/pact_broker/ui/views/layouts/main.haml
|
|
1095
1089
|
- lib/pact_broker/ui/views/matrix/show.haml
|
|
1090
|
+
- lib/pact_broker/unified_diff.rb
|
|
1096
1091
|
- lib/pact_broker/verifications/latest_verification_for_consumer_and_provider.rb
|
|
1097
1092
|
- lib/pact_broker/verifications/latest_verification_for_consumer_version_tag.rb
|
|
1098
1093
|
- lib/pact_broker/verifications/latest_verification_id_for_pact_version_and_provider_version.rb
|
data/lib/pact/doc/doc_file.rb
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
module Pact
|
|
2
|
-
module Doc
|
|
3
|
-
class DocFile
|
|
4
|
-
|
|
5
|
-
def initialize consumer_contract, dir, consumer_contract_renderer, file_extension
|
|
6
|
-
@dir = dir
|
|
7
|
-
@consumer_contract = consumer_contract
|
|
8
|
-
@consumer_contract_renderer = consumer_contract_renderer
|
|
9
|
-
@file_extension = file_extension
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def write
|
|
13
|
-
File.open(path, "w") { |io| io << doc_file_contents }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def title
|
|
17
|
-
consumer_contract.provider.name
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def name
|
|
21
|
-
"#{consumer_contract.consumer.name} - #{consumer_contract.provider.name}#{file_extension}"
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
attr_reader :dir, :consumer_contract, :consumer_contract_renderer, :file_extension
|
|
27
|
-
|
|
28
|
-
def path
|
|
29
|
-
File.join(dir, name)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def doc_file_contents
|
|
33
|
-
consumer_contract_renderer.call(consumer_contract)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
data/lib/pact/doc/generate.rb
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
module Pact
|
|
2
|
-
module Doc
|
|
3
|
-
class Generate
|
|
4
|
-
|
|
5
|
-
def self.call pact_dir = Pact.configuration.pact_dir, doc_dir = Pact.configuration.doc_dir, doc_generators = Pact.configuration.doc_generators
|
|
6
|
-
doc_generators.each{| doc_generator| doc_generator.call pact_dir, doc_dir }
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
data/lib/pact/doc/generator.rb
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
require "pact/doc/doc_file"
|
|
2
|
-
require "fileutils"
|
|
3
|
-
|
|
4
|
-
module Pact
|
|
5
|
-
module Doc
|
|
6
|
-
|
|
7
|
-
class Generator
|
|
8
|
-
|
|
9
|
-
def initialize pact_dir, doc_dir, options
|
|
10
|
-
@doc_dir = doc_dir
|
|
11
|
-
@pact_dir = pact_dir
|
|
12
|
-
@consumer_contract_renderer = options[:consumer_contract_renderer]
|
|
13
|
-
@doc_type = options[:doc_type]
|
|
14
|
-
@file_extension = options[:file_extension]
|
|
15
|
-
@index_renderer = options[:index_renderer]
|
|
16
|
-
@index_name = options[:index_name]
|
|
17
|
-
@after = options.fetch(:after, lambda{|pact_dir_arg, target_dir_arg, consumer_contracts_arg| })
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def call
|
|
21
|
-
ensure_target_dir_exists_and_is_clean
|
|
22
|
-
write_index if consumer_contracts.any?
|
|
23
|
-
write_doc_files
|
|
24
|
-
perform_after_hook
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
attr_reader :doc_dir, :pact_dir, :consumer_contract_renderer, :doc_type, :file_extension, :index_renderer, :after
|
|
30
|
-
|
|
31
|
-
def write_index
|
|
32
|
-
File.open(index_file_path, "w") { |io| io << index_file_contents }
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def index_file_path
|
|
36
|
-
File.join(target_dir, "#{@index_name}#{file_extension}")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def index_file_contents
|
|
40
|
-
index_renderer.call(consumer_contracts.first.consumer.name, index_data)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def index_data
|
|
44
|
-
doc_files.each_with_object({}) do | doc_file, data |
|
|
45
|
-
data[doc_file.title] = doc_file.name
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def write_doc_files
|
|
50
|
-
doc_files.each(&:write)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def doc_files
|
|
54
|
-
consumer_contracts.collect do | consumer_contract |
|
|
55
|
-
DocFile.new(consumer_contract, target_dir, consumer_contract_renderer, file_extension)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def consumer_contracts
|
|
60
|
-
@consumer_contracts ||= begin
|
|
61
|
-
Dir.glob("#{pact_dir}/**").collect do |file|
|
|
62
|
-
Pact::ConsumerContract.from_uri file
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def perform_after_hook
|
|
68
|
-
after.call(pact_dir, target_dir, consumer_contracts)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def ensure_target_dir_exists_and_is_clean
|
|
72
|
-
FileUtils.rm_rf target_dir
|
|
73
|
-
FileUtils.mkdir_p target_dir
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def target_dir
|
|
77
|
-
File.join(doc_dir, doc_type)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
require "pact/shared/active_support_support"
|
|
2
|
-
require "pact/reification"
|
|
3
|
-
require "cgi"
|
|
4
|
-
require "pact/doc"
|
|
5
|
-
|
|
6
|
-
module Pact
|
|
7
|
-
module Doc
|
|
8
|
-
class InteractionViewModel
|
|
9
|
-
|
|
10
|
-
include Pact::ActiveSupportSupport
|
|
11
|
-
|
|
12
|
-
MARKDOWN_BOLD_CHARACTERS = "**"
|
|
13
|
-
|
|
14
|
-
def initialize interaction, consumer_contract
|
|
15
|
-
@interaction = interaction
|
|
16
|
-
@consumer_contract = consumer_contract
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def id
|
|
20
|
-
@id ||= begin
|
|
21
|
-
full_desc = if has_provider_state?
|
|
22
|
-
"#{description} given #{interaction.provider_state}"
|
|
23
|
-
else
|
|
24
|
-
description
|
|
25
|
-
end
|
|
26
|
-
CGI.escapeHTML(full_desc.gsub(/\s+/,"_"))
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def request_method
|
|
31
|
-
interaction.request.method.upcase
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def request_path
|
|
35
|
-
interaction.request.path
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def response_status
|
|
39
|
-
interaction.response.status
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def consumer_name
|
|
43
|
-
markdown_escape @consumer_contract.consumer.name
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def provider_name
|
|
47
|
-
markdown_escape @consumer_contract.provider.name
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def has_provider_state?
|
|
51
|
-
@interaction.provider_state && !@interaction.provider_state.empty?
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def provider_state start_of_sentence = false
|
|
55
|
-
markdown_escape apply_capitals(@interaction.provider_state.strip, start_of_sentence)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def formatted_provider_states mark_bold: false
|
|
59
|
-
bold_marker = mark_bold ? MARKDOWN_BOLD_CHARACTERS : ""
|
|
60
|
-
|
|
61
|
-
(@interaction.provider_states || []).map do |ps|
|
|
62
|
-
"#{bold_marker}" \
|
|
63
|
-
"#{markdown_escape(apply_capitals(ps.name.strip, false))}" \
|
|
64
|
-
"#{bold_marker}"
|
|
65
|
-
end.join(" and ")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def description start_of_sentence = false
|
|
69
|
-
return "" unless @interaction.description
|
|
70
|
-
markdown_escape apply_capitals(@interaction.description.strip, start_of_sentence)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def request
|
|
74
|
-
the_clean_request = clean_request
|
|
75
|
-
if the_clean_request == "ASYNC_REQUEST"
|
|
76
|
-
"ASYNC_REQUEST"
|
|
77
|
-
else
|
|
78
|
-
fix_json_formatting JSON.pretty_generate(clean_request)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def response
|
|
83
|
-
fix_json_formatting JSON.pretty_generate(clean_response)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
attr_reader :interaction, :consumer_contract
|
|
89
|
-
|
|
90
|
-
def clean_request
|
|
91
|
-
reified = Reification.from_term(interaction.request)
|
|
92
|
-
return "ASYNC_REQUEST" if reified[:method] == "FAKE_ASYNC_METHOD"
|
|
93
|
-
|
|
94
|
-
ordered = ordered_clean_hash(reified).tap do |h|
|
|
95
|
-
h[:body] = reified[:body] if reified[:body]
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
return ordered[:body].to_h.slice("contents", "metadata") if ordered[:method] == "FAKE_SYNC_METHOD"
|
|
99
|
-
|
|
100
|
-
ordered
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def clean_response
|
|
104
|
-
raw = Reification.from_term(interaction.response)
|
|
105
|
-
|
|
106
|
-
case raw[:status]
|
|
107
|
-
when "FAKE_ASYNC_METHOD"
|
|
108
|
-
raw[:body] || {}
|
|
109
|
-
when "FAKE_SYNC_METHOD"
|
|
110
|
-
Array(raw.dig(:body, "contents")).map { |item| item.slice("contents", "metadata") }
|
|
111
|
-
else
|
|
112
|
-
ordered_clean_hash(raw)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Remove empty body and headers hashes from response, and empty headers from request,
|
|
117
|
-
# as an empty hash means "allow anything" - it's more intuitive and cleaner to just
|
|
118
|
-
# remove the empty hashes from display.
|
|
119
|
-
def ordered_clean_hash source
|
|
120
|
-
ordered_keys.each_with_object({}) do |key, target|
|
|
121
|
-
if source.key? key
|
|
122
|
-
target[key] = source[key] unless value_is_an_empty_hash(source[key])
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def value_is_an_empty_hash value
|
|
128
|
-
value.is_a?(Hash) && value.empty?
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def ordered_keys
|
|
132
|
-
[:method, :path, :query, :status, :headers, :body]
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def remove_key_if_empty key, hash
|
|
136
|
-
hash.delete(key) if hash[key].is_a?(Hash) && hash[key].empty?
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def apply_capitals string, start_of_sentence = false
|
|
140
|
-
start_of_sentence ? capitalize_first_letter(string) : lowercase_first_letter(string)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def capitalize_first_letter string
|
|
144
|
-
string[0].upcase + string[1..-1]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def lowercase_first_letter string
|
|
148
|
-
string[0].downcase + string[1..-1]
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def markdown_escape string
|
|
152
|
-
return nil unless string
|
|
153
|
-
string.gsub(Pact::Doc::MARKDOWN_SPECIAL_CHARS_REGEXP) { |char| "\\#{char}" }
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
require "pact/doc/markdown/interaction_renderer"
|
|
2
|
-
require "pact/doc/sort_interactions"
|
|
3
|
-
require "rack/utils"
|
|
4
|
-
require "pact/doc"
|
|
5
|
-
|
|
6
|
-
module Pact
|
|
7
|
-
module Doc
|
|
8
|
-
module Markdown
|
|
9
|
-
class ConsumerContractRenderer
|
|
10
|
-
|
|
11
|
-
def initialize consumer_contract
|
|
12
|
-
@consumer_contract = consumer_contract
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.call consumer_contract
|
|
16
|
-
new(consumer_contract).call
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call
|
|
20
|
-
title + summaries_title + summaries + interactions_title + full_interactions
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
attr_reader :consumer_contract
|
|
26
|
-
|
|
27
|
-
def title
|
|
28
|
-
"# A pact between #{consumer_name} and #{provider_name}\n\n"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def interaction_renderers
|
|
32
|
-
@interaction_renderers ||= sorted_interactions.collect{|interaction| InteractionRenderer.new interaction, @consumer_contract}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def summaries_title
|
|
36
|
-
"### Requests from #{consumer_name} to #{provider_name}\n\n"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def interactions_title
|
|
40
|
-
"### Interactions\n\n"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def summaries
|
|
44
|
-
interaction_renderers.collect(&:render_summary).join
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def full_interactions
|
|
48
|
-
interaction_renderers.collect(&:render_full_interaction).join
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def sorted_interactions
|
|
52
|
-
SortInteractions.call(consumer_contract.interactions)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def consumer_name
|
|
56
|
-
h(markdown_escape consumer_contract.consumer.name)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def provider_name
|
|
60
|
-
h(markdown_escape consumer_contract.provider.name)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def markdown_escape string
|
|
64
|
-
return nil unless string
|
|
65
|
-
string.gsub(Pact::Doc::MARKDOWN_SPECIAL_CHARS_REGEXP) { |char| "\\#{char}" }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def h(text)
|
|
69
|
-
Rack::Utils.escape_html(text)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
require "pact/doc/generator"
|
|
2
|
-
require "pact/doc/markdown/consumer_contract_renderer"
|
|
3
|
-
require "pact/doc/markdown/index_renderer"
|
|
4
|
-
|
|
5
|
-
module Pact
|
|
6
|
-
module Doc
|
|
7
|
-
module Markdown
|
|
8
|
-
class Generator < Pact::Doc::Generator
|
|
9
|
-
def initialize pact_dir, doc_dir
|
|
10
|
-
super(pact_dir, doc_dir,
|
|
11
|
-
consumer_contract_renderer: ConsumerContractRenderer,
|
|
12
|
-
doc_type: "markdown",
|
|
13
|
-
file_extension: ".md",
|
|
14
|
-
index_renderer: IndexRenderer,
|
|
15
|
-
index_name: "README")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.call pact_dir, doc_dir
|
|
19
|
-
new(pact_dir, doc_dir).call
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Pact
|
|
2
|
-
module Doc
|
|
3
|
-
module Markdown
|
|
4
|
-
class IndexRenderer
|
|
5
|
-
attr_reader :consumer_name
|
|
6
|
-
attr_reader :docs # Hash of pact title => file_name
|
|
7
|
-
|
|
8
|
-
def initialize consumer_name, docs
|
|
9
|
-
@consumer_name = consumer_name
|
|
10
|
-
@docs = docs
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.call consumer_name, docs
|
|
14
|
-
new(consumer_name, docs).call
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def call
|
|
18
|
-
title + "\n\n" + table_of_contents + "\n"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def table_of_contents
|
|
24
|
-
docs.collect do | title, file_name |
|
|
25
|
-
item title, file_name
|
|
26
|
-
end.join("\n")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def title
|
|
30
|
-
"### Pacts for #{consumer_name}"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def item title, file_name
|
|
34
|
-
"* [#{title}](#{file_name})"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
require "erb"
|
|
2
|
-
require "pact/doc/interaction_view_model"
|
|
3
|
-
require "rack/utils"
|
|
4
|
-
|
|
5
|
-
module Pact
|
|
6
|
-
module Doc
|
|
7
|
-
module Markdown
|
|
8
|
-
class InteractionRenderer
|
|
9
|
-
attr_reader :interaction
|
|
10
|
-
|
|
11
|
-
def initialize interaction, pact
|
|
12
|
-
@interaction = InteractionViewModel.new(interaction, pact)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def render_summary
|
|
16
|
-
formatted_provider_states = h(interaction.formatted_provider_states)
|
|
17
|
-
suffix = formatted_provider_states.empty? ? "" : " given #{formatted_provider_states}"
|
|
18
|
-
"* [#{h(interaction.description(true))}](##{interaction.id})#{suffix}\n\n"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def render_full_interaction
|
|
22
|
-
if interaction.request == "ASYNC_REQUEST"
|
|
23
|
-
render("/interaction_async.erb")
|
|
24
|
-
else
|
|
25
|
-
render("/interaction.erb")
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def render template_file
|
|
30
|
-
ERB.new(template_string(template_file)).result(binding)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# The template file is written with only ASCII range characters, so we
|
|
34
|
-
# can read as UTF-8. But rendered strings must have same encoding as
|
|
35
|
-
# script encoding because it will joined to strings which are produced by
|
|
36
|
-
# string literal.
|
|
37
|
-
def template_string(template_file)
|
|
38
|
-
File.read(template_contents(template_file), external_encoding: Encoding::UTF_8).force_encoding(__ENCODING__)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def template_contents(template_file)
|
|
42
|
-
File.dirname(__FILE__) + template_file
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def h(text)
|
|
46
|
-
Rack::Utils.escape_html(text)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Pact
|
|
2
|
-
module Doc
|
|
3
|
-
class SortInteractions
|
|
4
|
-
def self.call interactions
|
|
5
|
-
interactions.sort{|a, b| sortable_id(a) <=> sortable_id(b)}
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def self.sortable_id interaction
|
|
10
|
-
"#{(interaction.description || '').downcase} #{interaction.response.status} #{(interaction.provider_state || '').downcase}"
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
data/lib/pact/doc.rb
DELETED
|
File without changes
|
|
File without changes
|