pact 1.67.5 → 2.0.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.
Files changed (240) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/lib/pact/configuration.rb +21 -0
  4. data/lib/pact/consumer/grpc_interaction_builder.rb +192 -0
  5. data/lib/pact/consumer/http_interaction_builder.rb +160 -0
  6. data/lib/pact/consumer/interaction_contents.rb +62 -0
  7. data/lib/pact/consumer/message_interaction_builder.rb +286 -0
  8. data/lib/pact/consumer/mock_server.rb +106 -0
  9. data/lib/pact/consumer/pact_config/base.rb +22 -0
  10. data/lib/pact/consumer/pact_config/grpc.rb +24 -0
  11. data/lib/pact/consumer/pact_config/http.rb +53 -0
  12. data/lib/pact/consumer/pact_config/message.rb +15 -0
  13. data/lib/pact/consumer/pact_config/plugin_async_message.rb +24 -0
  14. data/lib/pact/consumer/pact_config/plugin_http.rb +24 -0
  15. data/lib/pact/consumer/pact_config/plugin_sync_message.rb +24 -0
  16. data/lib/pact/consumer/pact_config.rb +28 -0
  17. data/lib/pact/consumer/plugin_async_message_interaction_builder.rb +169 -0
  18. data/lib/pact/consumer/plugin_http_interaction_builder.rb +199 -0
  19. data/lib/pact/consumer/plugin_sync_message_interaction_builder.rb +178 -0
  20. data/lib/pact/consumer.rb +6 -7
  21. data/lib/pact/generators/base.rb +285 -0
  22. data/lib/pact/generators.rb +47 -0
  23. data/lib/pact/matchers/base.rb +72 -0
  24. data/lib/pact/matchers/v1/equality.rb +17 -0
  25. data/lib/pact/matchers/v2/regex.rb +26 -0
  26. data/lib/pact/matchers/v2/type.rb +18 -0
  27. data/lib/pact/matchers/v3/boolean.rb +18 -0
  28. data/lib/pact/matchers/v3/content_type.rb +30 -0
  29. data/lib/pact/matchers/v3/date.rb +22 -0
  30. data/lib/pact/matchers/v3/date_time.rb +22 -0
  31. data/lib/pact/matchers/v3/decimal.rb +18 -0
  32. data/lib/pact/matchers/v3/each.rb +46 -0
  33. data/lib/pact/matchers/v3/include.rb +18 -0
  34. data/lib/pact/matchers/v3/integer.rb +18 -0
  35. data/lib/pact/matchers/v3/null.rb +13 -0
  36. data/lib/pact/matchers/v3/number.rb +18 -0
  37. data/lib/pact/matchers/v3/semver.rb +22 -0
  38. data/lib/pact/matchers/v3/time.rb +22 -0
  39. data/lib/pact/matchers/v3/values.rb +13 -0
  40. data/lib/pact/matchers/v4/each_key.rb +34 -0
  41. data/lib/pact/matchers/v4/each_key_value.rb +38 -0
  42. data/lib/pact/matchers/v4/each_value.rb +40 -0
  43. data/lib/pact/matchers/v4/not_empty.rb +22 -0
  44. data/lib/pact/matchers/v4/status_code.rb +15 -0
  45. data/lib/pact/matchers.rb +108 -0
  46. data/lib/pact/native/blocking_verifier.rb +15 -0
  47. data/lib/pact/native/logger.rb +24 -0
  48. data/lib/pact/provider/async_message_verifier.rb +29 -0
  49. data/lib/pact/provider/base_verifier.rb +259 -0
  50. data/lib/pact/provider/grpc_verifier.rb +40 -0
  51. data/lib/pact/provider/gruf_server.rb +73 -0
  52. data/lib/pact/provider/http_server.rb +77 -0
  53. data/lib/pact/provider/http_verifier.rb +45 -0
  54. data/lib/pact/provider/message_provider_servlet.rb +77 -0
  55. data/lib/pact/provider/mixed_verifier.rb +21 -0
  56. data/lib/pact/provider/pact_broker_proxy.rb +62 -0
  57. data/lib/pact/provider/pact_broker_proxy_runner.rb +78 -0
  58. data/lib/pact/provider/pact_config/async.rb +28 -0
  59. data/lib/pact/provider/pact_config/base.rb +101 -0
  60. data/lib/pact/provider/pact_config/grpc.rb +24 -0
  61. data/lib/pact/provider/pact_config/http.rb +25 -0
  62. data/lib/pact/provider/pact_config/mixed.rb +38 -0
  63. data/lib/pact/provider/pact_config.rb +24 -0
  64. data/lib/pact/provider/provider_server_runner.rb +90 -0
  65. data/lib/pact/provider/provider_state_configuration.rb +32 -0
  66. data/lib/pact/provider/provider_state_servlet.rb +84 -0
  67. data/lib/pact/provider.rb +6 -3
  68. data/lib/pact/railtie.rb +11 -0
  69. data/lib/pact/{v2/rspec → rspec}/support/pact_consumer_helpers.rb +8 -8
  70. data/lib/pact/{v2/rspec → rspec}/support/pact_provider_helpers.rb +25 -24
  71. data/lib/pact/{v2/rspec → rspec}/support/webmock/webmock_helpers.rb +1 -1
  72. data/lib/pact/{v2/rspec.rb → rspec.rb} +6 -5
  73. data/lib/pact/support/core_ext.rb +93 -0
  74. data/lib/pact/tasks/pact.rake +13 -0
  75. data/lib/pact/version.rb +1 -1
  76. data/lib/pact.rb +52 -11
  77. data/pact.gemspec +47 -64
  78. metadata +146 -404
  79. data/lib/pact/cli/generate_pact_docs.rb +0 -4
  80. data/lib/pact/cli/run_pact_verification.rb +0 -99
  81. data/lib/pact/cli/spec_criteria.rb +0 -26
  82. data/lib/pact/cli.rb +0 -45
  83. data/lib/pact/consumer/configuration/configuration_extensions.rb +0 -90
  84. data/lib/pact/consumer/configuration/dsl.rb +0 -11
  85. data/lib/pact/consumer/configuration/mock_service.rb +0 -112
  86. data/lib/pact/consumer/configuration/service_consumer.rb +0 -51
  87. data/lib/pact/consumer/configuration/service_provider.rb +0 -40
  88. data/lib/pact/consumer/configuration.rb +0 -10
  89. data/lib/pact/consumer/consumer_contract_builder.rb +0 -82
  90. data/lib/pact/consumer/consumer_contract_builders.rb +0 -10
  91. data/lib/pact/consumer/interaction_builder.rb +0 -45
  92. data/lib/pact/consumer/rspec.rb +0 -35
  93. data/lib/pact/consumer/spec_hooks.rb +0 -40
  94. data/lib/pact/consumer/world.rb +0 -37
  95. data/lib/pact/doc/README.md +0 -13
  96. data/lib/pact/doc/doc_file.rb +0 -40
  97. data/lib/pact/doc/generate.rb +0 -11
  98. data/lib/pact/doc/generator.rb +0 -82
  99. data/lib/pact/doc/interaction_view_model.rb +0 -124
  100. data/lib/pact/doc/markdown/consumer_contract_renderer.rb +0 -68
  101. data/lib/pact/doc/markdown/generator.rb +0 -26
  102. data/lib/pact/doc/markdown/index_renderer.rb +0 -43
  103. data/lib/pact/doc/markdown/interaction.erb +0 -14
  104. data/lib/pact/doc/markdown/interaction_renderer.rb +0 -43
  105. data/lib/pact/doc/sort_interactions.rb +0 -16
  106. data/lib/pact/hal/authorization_header_redactor.rb +0 -32
  107. data/lib/pact/hal/entity.rb +0 -110
  108. data/lib/pact/hal/http_client.rb +0 -146
  109. data/lib/pact/hal/link.rb +0 -112
  110. data/lib/pact/hal/non_json_entity.rb +0 -28
  111. data/lib/pact/hash_refinements.rb +0 -17
  112. data/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +0 -112
  113. data/lib/pact/pact_broker/fetch_pacts.rb +0 -103
  114. data/lib/pact/pact_broker/notices.rb +0 -34
  115. data/lib/pact/pact_broker/pact_selection_description.rb +0 -66
  116. data/lib/pact/pact_broker.rb +0 -25
  117. data/lib/pact/project_root.rb +0 -7
  118. data/lib/pact/provider/configuration/configuration_extension.rb +0 -69
  119. data/lib/pact/provider/configuration/dsl.rb +0 -18
  120. data/lib/pact/provider/configuration/message_provider_dsl.rb +0 -63
  121. data/lib/pact/provider/configuration/pact_verification.rb +0 -48
  122. data/lib/pact/provider/configuration/pact_verification_from_broker.rb +0 -126
  123. data/lib/pact/provider/configuration/service_provider_config.rb +0 -32
  124. data/lib/pact/provider/configuration/service_provider_dsl.rb +0 -107
  125. data/lib/pact/provider/configuration.rb +0 -7
  126. data/lib/pact/provider/context.rb +0 -0
  127. data/lib/pact/provider/help/console_text.rb +0 -76
  128. data/lib/pact/provider/help/content.rb +0 -38
  129. data/lib/pact/provider/help/pact_diff.rb +0 -43
  130. data/lib/pact/provider/help/prompt_text.rb +0 -49
  131. data/lib/pact/provider/help/write.rb +0 -56
  132. data/lib/pact/provider/matchers/messages.rb +0 -66
  133. data/lib/pact/provider/pact_helper_locator.rb +0 -24
  134. data/lib/pact/provider/pact_source.rb +0 -40
  135. data/lib/pact/provider/pact_spec_runner.rb +0 -188
  136. data/lib/pact/provider/pact_uri.rb +0 -55
  137. data/lib/pact/provider/pact_verification.rb +0 -17
  138. data/lib/pact/provider/print_missing_provider_states.rb +0 -35
  139. data/lib/pact/provider/request.rb +0 -90
  140. data/lib/pact/provider/rspec/backtrace_formatter.rb +0 -43
  141. data/lib/pact/provider/rspec/calculate_exit_code.rb +0 -18
  142. data/lib/pact/provider/rspec/custom_options_file +0 -0
  143. data/lib/pact/provider/rspec/formatter_rspec_2.rb +0 -76
  144. data/lib/pact/provider/rspec/formatter_rspec_3.rb +0 -195
  145. data/lib/pact/provider/rspec/json_formatter.rb +0 -100
  146. data/lib/pact/provider/rspec/matchers.rb +0 -80
  147. data/lib/pact/provider/rspec/pact_broker_formatter.rb +0 -76
  148. data/lib/pact/provider/rspec.rb +0 -234
  149. data/lib/pact/provider/state/provider_state.rb +0 -180
  150. data/lib/pact/provider/state/provider_state_configured_modules.rb +0 -15
  151. data/lib/pact/provider/state/provider_state_manager.rb +0 -42
  152. data/lib/pact/provider/state/provider_state_proxy.rb +0 -39
  153. data/lib/pact/provider/state/set_up.rb +0 -13
  154. data/lib/pact/provider/state/tear_down.rb +0 -13
  155. data/lib/pact/provider/test_methods.rb +0 -77
  156. data/lib/pact/provider/verification_report.rb +0 -36
  157. data/lib/pact/provider/verification_results/create.rb +0 -88
  158. data/lib/pact/provider/verification_results/publish.rb +0 -143
  159. data/lib/pact/provider/verification_results/publish_all.rb +0 -50
  160. data/lib/pact/provider/verification_results/verification_result.rb +0 -40
  161. data/lib/pact/provider/world.rb +0 -50
  162. data/lib/pact/retry.rb +0 -37
  163. data/lib/pact/tasks/task_helper.rb +0 -62
  164. data/lib/pact/tasks/verification_task.rb +0 -95
  165. data/lib/pact/tasks.rb +0 -2
  166. data/lib/pact/templates/help.erb +0 -22
  167. data/lib/pact/templates/provider_state.erb +0 -14
  168. data/lib/pact/utils/metrics.rb +0 -100
  169. data/lib/pact/utils/string.rb +0 -35
  170. data/lib/pact/v2/configuration.rb +0 -23
  171. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +0 -194
  172. data/lib/pact/v2/consumer/http_interaction_builder.rb +0 -162
  173. data/lib/pact/v2/consumer/interaction_contents.rb +0 -62
  174. data/lib/pact/v2/consumer/message_interaction_builder.rb +0 -288
  175. data/lib/pact/v2/consumer/mock_server.rb +0 -108
  176. data/lib/pact/v2/consumer/pact_config/base.rb +0 -24
  177. data/lib/pact/v2/consumer/pact_config/grpc.rb +0 -26
  178. data/lib/pact/v2/consumer/pact_config/http.rb +0 -55
  179. data/lib/pact/v2/consumer/pact_config/message.rb +0 -17
  180. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +0 -26
  181. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +0 -26
  182. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +0 -26
  183. data/lib/pact/v2/consumer/pact_config.rb +0 -30
  184. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +0 -171
  185. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +0 -201
  186. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +0 -180
  187. data/lib/pact/v2/consumer.rb +0 -8
  188. data/lib/pact/v2/generators/base.rb +0 -287
  189. data/lib/pact/v2/generators.rb +0 -49
  190. data/lib/pact/v2/matchers/base.rb +0 -74
  191. data/lib/pact/v2/matchers/v1/equality.rb +0 -19
  192. data/lib/pact/v2/matchers/v2/regex.rb +0 -19
  193. data/lib/pact/v2/matchers/v2/type.rb +0 -17
  194. data/lib/pact/v2/matchers/v3/boolean.rb +0 -17
  195. data/lib/pact/v2/matchers/v3/content_type.rb +0 -32
  196. data/lib/pact/v2/matchers/v3/date.rb +0 -18
  197. data/lib/pact/v2/matchers/v3/date_time.rb +0 -18
  198. data/lib/pact/v2/matchers/v3/decimal.rb +0 -17
  199. data/lib/pact/v2/matchers/v3/each.rb +0 -42
  200. data/lib/pact/v2/matchers/v3/include.rb +0 -17
  201. data/lib/pact/v2/matchers/v3/integer.rb +0 -17
  202. data/lib/pact/v2/matchers/v3/null.rb +0 -16
  203. data/lib/pact/v2/matchers/v3/number.rb +0 -17
  204. data/lib/pact/v2/matchers/v3/semver.rb +0 -23
  205. data/lib/pact/v2/matchers/v3/time.rb +0 -18
  206. data/lib/pact/v2/matchers/v3/values.rb +0 -16
  207. data/lib/pact/v2/matchers/v4/each_key.rb +0 -26
  208. data/lib/pact/v2/matchers/v4/each_key_value.rb +0 -32
  209. data/lib/pact/v2/matchers/v4/each_value.rb +0 -33
  210. data/lib/pact/v2/matchers/v4/not_empty.rb +0 -24
  211. data/lib/pact/v2/matchers/v4/status_code.rb +0 -17
  212. data/lib/pact/v2/matchers.rb +0 -110
  213. data/lib/pact/v2/native/blocking_verifier.rb +0 -17
  214. data/lib/pact/v2/native/logger.rb +0 -25
  215. data/lib/pact/v2/provider/async_message_verifier.rb +0 -28
  216. data/lib/pact/v2/provider/base_verifier.rb +0 -242
  217. data/lib/pact/v2/provider/grpc_verifier.rb +0 -38
  218. data/lib/pact/v2/provider/gruf_server.rb +0 -75
  219. data/lib/pact/v2/provider/http_server.rb +0 -79
  220. data/lib/pact/v2/provider/http_verifier.rb +0 -43
  221. data/lib/pact/v2/provider/message_provider_servlet.rb +0 -79
  222. data/lib/pact/v2/provider/mixed_verifier.rb +0 -22
  223. data/lib/pact/v2/provider/pact_broker_proxy.rb +0 -66
  224. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +0 -80
  225. data/lib/pact/v2/provider/pact_config/async.rb +0 -29
  226. data/lib/pact/v2/provider/pact_config/base.rb +0 -103
  227. data/lib/pact/v2/provider/pact_config/grpc.rb +0 -26
  228. data/lib/pact/v2/provider/pact_config/http.rb +0 -27
  229. data/lib/pact/v2/provider/pact_config/mixed.rb +0 -40
  230. data/lib/pact/v2/provider/pact_config.rb +0 -26
  231. data/lib/pact/v2/provider/provider_server_runner.rb +0 -92
  232. data/lib/pact/v2/provider/provider_state_configuration.rb +0 -32
  233. data/lib/pact/v2/provider/provider_state_servlet.rb +0 -86
  234. data/lib/pact/v2/provider.rb +0 -8
  235. data/lib/pact/v2/railtie.rb +0 -13
  236. data/lib/pact/v2/tasks/pact.rake +0 -13
  237. data/lib/pact/v2.rb +0 -71
  238. data/lib/tasks/pact.rake +0 -34
  239. /data/lib/pact/{v2/rspec → rspec}/support/pact_message_helpers.rb +0 -0
  240. /data/lib/pact/{v2/rspec → rspec}/support/waterdrop/pact_waterdrop_client.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8be91a33981b95189c08f545d3746c76f2f2ad8642777f753cc3b414233986de
4
- data.tar.gz: 1bc7d8a6e8a08c47b79e73f68e0caed9585a0eafb0bc316a4e589f2b3f9028a3
3
+ metadata.gz: ae64ff5080cc286c9a913c8ad1e06d99c35961f687eabe3e99c5bcaac6078aeb
4
+ data.tar.gz: 8fdbd4c74eb3e5c2cf57300ce269cb7e48f52fb83e5886f8e7aee08e97274d9a
5
5
  SHA512:
6
- metadata.gz: a73f8cf2aa2956f7e0c7ed2dc8ab35fefbf1b7a2b0ab88d21bb3936a4761ae60d2fd622c4f2a767f2b6e75837556aeb055011667def9a90b8fbe1f9df74bf0f9
7
- data.tar.gz: e27d62a8822b43ded6ccd48b926c2833292617834c7c2b8dda45d93a26a769d1093909d87921f8c15962445d3ef2012b1df2d5417ed7a07259a4275862c9f4b9
6
+ metadata.gz: 7b01cb166524d3fdc62d7a34ecacd922281d63885cb4cc48cc4ba3d4d5007511d5b2263b6697060cc443fda3efcdec45d2d54d182e6b9f1e9a9d9f3ac593cb36
7
+ data.tar.gz: acbf67bc8d3b23c85fbbcda46cdbf29297fd2eb859430536d58537ff055fb64ac4d6f9c28d6fee78324ebd33b9e534c6dd2e26dd454376dea9b8ea8aa28eb7e6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ <a name="v2.0.1"></a>
2
+ ### v2.0.1 (2026-05-29)
3
+
4
+ #### Bug Fixes
5
+
6
+ * **example**
7
+ * remove combustion dependency from examples ([281b284](/../../commit/281b284))
8
+
9
+ * align core_ext polyfills more closely with active_support implementations ([f08b8ce](/../../commit/f08b8ce))
10
+ * correct Object#blank? and Object#present? polyfill implementations ([53580b9](/../../commit/53580b9))
11
+ * add deep_dup polyfill alongside blank?/present? ([b116e14](/../../commit/b116e14))
12
+ * remove hard requirements on active_support and webmock ([e77cd34](/../../commit/e77cd34))
13
+ * support scalar request and response bodies in InteractionContents ([5440ed1](/../../commit/5440ed1))
14
+
15
+ <a name="v2.0.0"></a>
16
+ ### v2.0.0 (2026-03-18)
17
+
18
+ #### Features
19
+
20
+ * **v2**
21
+ * move pact/v2 to pact namespace (#397) ([1cc97ec](/../../commit/1cc97ec))
22
+
1
23
  <a name="v1.67.5"></a>
2
24
  ### v1.67.5 (2026-02-13)
3
25
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ class Configuration
5
+ attr_reader :before_provider_state_proc, :after_provider_state_proc
6
+
7
+ class GlobalProviderConfigurationError < ::Pact::Error; end
8
+
9
+ def before_provider_state_setup(&block)
10
+ raise GlobalProviderConfigurationError, 'no block given' unless block
11
+
12
+ @before_provider_state_proc = block
13
+ end
14
+
15
+ def after_provider_state_teardown(&block)
16
+ raise GlobalProviderConfigurationError, 'no block given' unless block
17
+
18
+ @after_provider_state_proc = block
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/sync_message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+
7
+ module Pact
8
+ module Consumer
9
+ class GrpcInteractionBuilder
10
+ CONTENT_TYPE = "application/protobuf"
11
+ GRPC_CONTENT_TYPE = "application/grpc"
12
+ PROTOBUF_PLUGIN_NAME = "protobuf"
13
+ PROTOBUF_PLUGIN_VERSION = "0.6.5"
14
+
15
+ class PluginInitError < Pact::FfiError; end
16
+
17
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
18
+ INIT_PLUGIN_ERRORS = {
19
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
20
+ 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
21
+ 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
22
+ }.freeze
23
+
24
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
25
+ CREATE_INTERACTION_ERRORS = {
26
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
27
+ 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
28
+ 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
29
+ 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
30
+ 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
31
+ 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
32
+ }.freeze
33
+
34
+ class CreateInteractionError < Pact::FfiError; end
35
+
36
+ class InteractionMismatchesError < Pact::Error; end
37
+
38
+ class InteractionBuilderError < Pact::Error; end
39
+
40
+ def initialize(pact_config, description: nil)
41
+ @pact_config = pact_config
42
+ @description = description || ""
43
+ @proto_path = nil
44
+ @proto_include_dirs = []
45
+ @service_name = nil
46
+ @method_name = nil
47
+ @request = nil
48
+ @response = nil
49
+ @response_meta = nil
50
+ @provider_state_meta = nil
51
+ end
52
+
53
+ def with_service(proto_path, method, include_dirs = [])
54
+ raise InteractionBuilderError.new("invalid grpc method: cannot be blank") if method.blank?
55
+
56
+ service_name, method_name = method.split("/") || []
57
+ raise InteractionBuilderError.new("invalid grpc method: #{method}, should be like service/SomeMethod") if service_name.blank? || method_name.blank?
58
+
59
+ absolute_path = File.expand_path(proto_path)
60
+ raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path)
61
+
62
+ @proto_path = absolute_path
63
+ @service_name = service_name
64
+ @method_name = method_name
65
+ @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) }
66
+
67
+ self
68
+ end
69
+
70
+ def with_pact_protobuf_plugin_version(version)
71
+ raise InteractionBuilderError.new("version is required") if version.blank?
72
+
73
+ @proto_plugin_version = version
74
+ self
75
+ end
76
+
77
+ def given(provider_state, metadata = {})
78
+ @provider_state_meta = {provider_state => metadata}
79
+ self
80
+ end
81
+
82
+ def upon_receiving(description)
83
+ @description = description
84
+ self
85
+ end
86
+
87
+ def with_request(req_hash)
88
+ @request = InteractionContents.plugin(req_hash)
89
+ self
90
+ end
91
+
92
+ def will_respond_with(resp_hash)
93
+ @response = InteractionContents.plugin(resp_hash)
94
+ self
95
+ end
96
+
97
+ def will_respond_with_meta(meta_hash)
98
+ @response_meta = InteractionContents.plugin(meta_hash)
99
+ self
100
+ end
101
+
102
+ def interaction_json
103
+ result = {
104
+ "pact:proto": @proto_path,
105
+ "pact:proto-service": "#{@service_name}/#{@method_name}",
106
+ "pact:content-type": CONTENT_TYPE,
107
+ request: @request
108
+ }
109
+
110
+ result["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present?
111
+
112
+ result[:response] = @response if @response.is_a?(Hash)
113
+ result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash)
114
+
115
+ JSON.dump(result)
116
+ end
117
+
118
+ def validate!
119
+ raise InteractionBuilderError.new("uninitialized service params, use #with_service to configure") if @proto_path.blank? || @service_name.blank? || @method_name.blank?
120
+ raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash)
121
+ raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash)
122
+ end
123
+
124
+ def execute(&block)
125
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
126
+
127
+ validate!
128
+
129
+ pact_handle = init_pact
130
+ init_plugin!(pact_handle)
131
+
132
+ message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description)
133
+ @provider_state_meta&.each_pair do |provider_state, meta|
134
+ if meta.present?
135
+ meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) }
136
+ else
137
+ PactFfi.given(message_pact, provider_state)
138
+ end
139
+ end
140
+
141
+ result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, GRPC_CONTENT_TYPE, interaction_json)
142
+ if CREATE_INTERACTION_ERRORS[result].present?
143
+ error = CREATE_INTERACTION_ERRORS[result]
144
+ raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
145
+ end
146
+
147
+ mock_server = MockServer.create_for_grpc!(pact: pact_handle, host: @pact_config.mock_host, port: @pact_config.mock_port)
148
+
149
+ yield(message_pact, mock_server)
150
+
151
+ ensure
152
+ if mock_server.matched?
153
+ mock_server.write_pacts!(@pact_config.pact_dir)
154
+ else
155
+ msg = mismatches_error_msg(mock_server)
156
+ raise InteractionMismatchesError.new(msg)
157
+ end
158
+ @used = true
159
+ mock_server&.cleanup
160
+ PactFfi::PluginConsumer.cleanup_plugins(pact_handle)
161
+ PactFfi.free_pact_handle(pact_handle)
162
+ end
163
+
164
+ private
165
+
166
+ def mismatches_error_msg(mock_server)
167
+ rspec_example_desc = RSpec.current_example&.description
168
+ return "interaction for #{@service_name}/#{@method_name} has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank?
169
+
170
+ "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}"
171
+ end
172
+
173
+ def init_pact
174
+ handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
175
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
176
+ PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
177
+
178
+ Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
179
+
180
+ handle
181
+ end
182
+
183
+ def init_plugin!(pact_handle)
184
+ result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION)
185
+ return result if INIT_PLUGIN_ERRORS[result].blank?
186
+
187
+ error = INIT_PLUGIN_ERRORS[result]
188
+ raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/sync_message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+ require "json"
7
+
8
+ module Pact
9
+ module Consumer
10
+ class HttpInteractionBuilder
11
+
12
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
13
+ CREATE_INTERACTION_ERRORS = {
14
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
15
+ 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
16
+ 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
17
+ 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
18
+ 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
19
+ 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
20
+ }.freeze
21
+
22
+ class CreateInteractionError < Pact::FfiError; end
23
+
24
+ class InteractionMismatchesError < Pact::Error; end
25
+
26
+ class InteractionBuilderError < Pact::Error; end
27
+
28
+ class << self
29
+ def create_finalizer(pact_handle)
30
+ proc { PactFfi.free_pact_handle(pact_handle) }
31
+ end
32
+ end
33
+
34
+ def initialize(pact_config, description: nil)
35
+ @pact_config = pact_config
36
+ @description = description || ""
37
+
38
+ @pact_handle = pact_config.pact_handle ||= init_pact
39
+ @pact_interaction = PactFfi.new_interaction(pact_handle, @description)
40
+
41
+ ObjectSpace.define_finalizer(self, self.class.create_finalizer(pact_interaction))
42
+ end
43
+
44
+ def given(provider_state, metadata = {})
45
+ if metadata.present?
46
+ PactFfi.given_with_params(pact_interaction, provider_state, JSON.dump(metadata))
47
+ else
48
+ PactFfi.given(pact_interaction, provider_state)
49
+ end
50
+
51
+ self
52
+ end
53
+
54
+ def upon_receiving(description)
55
+ @description = description
56
+ PactFfi.upon_receiving(pact_interaction, @description)
57
+ self
58
+ end
59
+
60
+ def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil)
61
+ interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_REQUEST"]
62
+ PactFfi.with_request(pact_interaction, method.to_s, format_value(path))
63
+
64
+ # Processing as an array of hashes, allows us to consider duplicate keys
65
+ # which should be passed to the core, at a non 0 index
66
+ if query.is_a?(Array)
67
+ key_index = Hash.new(0)
68
+ query.each do |query_item|
69
+ InteractionContents.basic(query_item).each_pair do |key, value_item|
70
+ PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, key_index[key], format_value(value_item))
71
+ key_index[key] += 1
72
+ end
73
+ end
74
+ else
75
+ InteractionContents.basic(query).each_pair do |key, value_item|
76
+ PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, 0, format_value(value_item))
77
+ end
78
+ end
79
+
80
+ InteractionContents.basic(headers).each_pair do |key, value_item|
81
+ PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item))
82
+ end
83
+
84
+ if body
85
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value))
86
+ end
87
+
88
+ self
89
+ end
90
+
91
+ def will_respond_with(status: nil, headers: {}, body: nil)
92
+ interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_RESPONSE"]
93
+ PactFfi.response_status(pact_interaction, status)
94
+
95
+ InteractionContents.basic(headers).each_pair do |key, value_item|
96
+ PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item))
97
+ end
98
+
99
+ if body
100
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body).value))
101
+ end
102
+
103
+ self
104
+ end
105
+
106
+ def execute(&block)
107
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
108
+
109
+ mock_server = MockServer.create_for_http!(
110
+ pact: pact_handle, host: pact_config.mock_host, port: pact_config.mock_port
111
+ )
112
+
113
+ yield(mock_server)
114
+
115
+ ensure
116
+ if mock_server.matched?
117
+ mock_server.write_pacts!(pact_config.pact_dir)
118
+ else
119
+ msg = mismatches_error_msg(mock_server)
120
+ raise InteractionMismatchesError.new(msg)
121
+ end
122
+ @used = true
123
+ mock_server&.cleanup
124
+ # Reset the pact handle to allow for a new interaction to be built
125
+ # without previous interactions being included
126
+ @pact_config.reset_pact
127
+ end
128
+
129
+ private
130
+
131
+ attr_reader :pact_handle, :pact_interaction, :pact_config
132
+
133
+ def mismatches_error_msg(mock_server)
134
+ rspec_example_desc = RSpec.current_example&.description
135
+ mismatches = JSON.pretty_generate(JSON.parse(mock_server.mismatches))
136
+ mismatches_with_colored_keys = mismatches.gsub(/"([^"]+)":/) { |match| "\e[34m#{match}\e[0m" } # Blue keys / white values
137
+
138
+ "#{rspec_example_desc} has mismatches: #{mismatches_with_colored_keys}"
139
+ end
140
+
141
+ def init_pact
142
+ handle = PactFfi.new_pact(pact_config.consumer_name, pact_config.provider_name)
143
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_config.pact_specification}"])
144
+ PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
145
+
146
+ Pact::Native::Logger.log_to_stdout(pact_config.log_level)
147
+
148
+ handle
149
+ end
150
+
151
+ def format_value(obj)
152
+ return obj if obj.is_a?(String)
153
+
154
+ return JSON.dump({value: obj}) if obj.is_a?(Array)
155
+
156
+ JSON.dump(obj)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module Consumer
5
+ class InteractionContents < Hash
6
+ BASIC_FORMAT = :basic
7
+ PLUGIN_FORMAT = :plugin
8
+
9
+ attr_reader :format
10
+
11
+ def self.basic(contents_hash)
12
+ new(contents_hash, BASIC_FORMAT)
13
+ end
14
+
15
+ def self.plugin(contents_hash)
16
+ new(contents_hash, PLUGIN_FORMAT)
17
+ end
18
+
19
+ def initialize(contents_hash, format)
20
+ serialized = init_hash(contents_hash, format)
21
+ # A scalar body (plain string, integer, etc.) serializes to a non-Hash
22
+ # value that cannot be merged pair by pair; expose it via #value instead.
23
+ if serialized.is_a?(Hash)
24
+ serialized.each_pair { |k, v| self[k] = v }
25
+ else
26
+ @value = serialized
27
+ end
28
+ @format = format
29
+ end
30
+
31
+ def value
32
+ defined?(@value) ? @value : self
33
+ end
34
+
35
+ private
36
+
37
+ def serialize(hash, format)
38
+ # serialize recursively
39
+ if hash.is_a?(Pact::Matchers::Base) || hash.is_a?(Pact::Generators::Base)
40
+ return hash.as_basic if format == :basic
41
+ return hash.as_plugin if format == :plugin
42
+ end
43
+
44
+ return hash.map { |value| serialize(value, format) } if hash.is_a?(Array)
45
+
46
+ # A value that is not a collection or a matcher/generator has nothing to
47
+ # recurse into, so return it unchanged (string, integer, boolean, nil, ...).
48
+ return hash unless hash.is_a?(Hash)
49
+
50
+ hash.each_pair do |key, value|
51
+ hash[key] = serialize(value, format)
52
+ end
53
+
54
+ hash
55
+ end
56
+
57
+ def init_hash(hash, format)
58
+ serialize(hash.deep_dup, format)
59
+ end
60
+ end
61
+ end
62
+ end