pact 1.67.4 → 2.0.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.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -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 +60 -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.rb → rspec.rb} +6 -5
  72. data/lib/pact/tasks/pact.rake +13 -0
  73. data/lib/pact/version.rb +1 -1
  74. data/lib/pact.rb +46 -11
  75. data/pact.gemspec +42 -65
  76. metadata +145 -404
  77. data/lib/pact/cli/generate_pact_docs.rb +0 -4
  78. data/lib/pact/cli/run_pact_verification.rb +0 -99
  79. data/lib/pact/cli/spec_criteria.rb +0 -26
  80. data/lib/pact/cli.rb +0 -45
  81. data/lib/pact/consumer/configuration/configuration_extensions.rb +0 -90
  82. data/lib/pact/consumer/configuration/dsl.rb +0 -11
  83. data/lib/pact/consumer/configuration/mock_service.rb +0 -112
  84. data/lib/pact/consumer/configuration/service_consumer.rb +0 -51
  85. data/lib/pact/consumer/configuration/service_provider.rb +0 -40
  86. data/lib/pact/consumer/configuration.rb +0 -10
  87. data/lib/pact/consumer/consumer_contract_builder.rb +0 -82
  88. data/lib/pact/consumer/consumer_contract_builders.rb +0 -10
  89. data/lib/pact/consumer/interaction_builder.rb +0 -45
  90. data/lib/pact/consumer/rspec.rb +0 -35
  91. data/lib/pact/consumer/spec_hooks.rb +0 -40
  92. data/lib/pact/consumer/world.rb +0 -37
  93. data/lib/pact/doc/README.md +0 -13
  94. data/lib/pact/doc/doc_file.rb +0 -40
  95. data/lib/pact/doc/generate.rb +0 -11
  96. data/lib/pact/doc/generator.rb +0 -82
  97. data/lib/pact/doc/interaction_view_model.rb +0 -124
  98. data/lib/pact/doc/markdown/consumer_contract_renderer.rb +0 -68
  99. data/lib/pact/doc/markdown/generator.rb +0 -26
  100. data/lib/pact/doc/markdown/index_renderer.rb +0 -43
  101. data/lib/pact/doc/markdown/interaction.erb +0 -14
  102. data/lib/pact/doc/markdown/interaction_renderer.rb +0 -43
  103. data/lib/pact/doc/sort_interactions.rb +0 -16
  104. data/lib/pact/hal/authorization_header_redactor.rb +0 -32
  105. data/lib/pact/hal/entity.rb +0 -110
  106. data/lib/pact/hal/http_client.rb +0 -128
  107. data/lib/pact/hal/link.rb +0 -112
  108. data/lib/pact/hal/non_json_entity.rb +0 -28
  109. data/lib/pact/hash_refinements.rb +0 -17
  110. data/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +0 -112
  111. data/lib/pact/pact_broker/fetch_pacts.rb +0 -103
  112. data/lib/pact/pact_broker/notices.rb +0 -34
  113. data/lib/pact/pact_broker/pact_selection_description.rb +0 -66
  114. data/lib/pact/pact_broker.rb +0 -25
  115. data/lib/pact/project_root.rb +0 -7
  116. data/lib/pact/provider/configuration/configuration_extension.rb +0 -69
  117. data/lib/pact/provider/configuration/dsl.rb +0 -18
  118. data/lib/pact/provider/configuration/message_provider_dsl.rb +0 -63
  119. data/lib/pact/provider/configuration/pact_verification.rb +0 -48
  120. data/lib/pact/provider/configuration/pact_verification_from_broker.rb +0 -126
  121. data/lib/pact/provider/configuration/service_provider_config.rb +0 -32
  122. data/lib/pact/provider/configuration/service_provider_dsl.rb +0 -107
  123. data/lib/pact/provider/configuration.rb +0 -7
  124. data/lib/pact/provider/context.rb +0 -0
  125. data/lib/pact/provider/help/console_text.rb +0 -76
  126. data/lib/pact/provider/help/content.rb +0 -38
  127. data/lib/pact/provider/help/pact_diff.rb +0 -43
  128. data/lib/pact/provider/help/prompt_text.rb +0 -49
  129. data/lib/pact/provider/help/write.rb +0 -56
  130. data/lib/pact/provider/matchers/messages.rb +0 -66
  131. data/lib/pact/provider/pact_helper_locator.rb +0 -24
  132. data/lib/pact/provider/pact_source.rb +0 -40
  133. data/lib/pact/provider/pact_spec_runner.rb +0 -188
  134. data/lib/pact/provider/pact_uri.rb +0 -55
  135. data/lib/pact/provider/pact_verification.rb +0 -17
  136. data/lib/pact/provider/print_missing_provider_states.rb +0 -35
  137. data/lib/pact/provider/request.rb +0 -90
  138. data/lib/pact/provider/rspec/backtrace_formatter.rb +0 -43
  139. data/lib/pact/provider/rspec/calculate_exit_code.rb +0 -18
  140. data/lib/pact/provider/rspec/custom_options_file +0 -0
  141. data/lib/pact/provider/rspec/formatter_rspec_2.rb +0 -76
  142. data/lib/pact/provider/rspec/formatter_rspec_3.rb +0 -195
  143. data/lib/pact/provider/rspec/json_formatter.rb +0 -100
  144. data/lib/pact/provider/rspec/matchers.rb +0 -80
  145. data/lib/pact/provider/rspec/pact_broker_formatter.rb +0 -76
  146. data/lib/pact/provider/rspec.rb +0 -234
  147. data/lib/pact/provider/state/provider_state.rb +0 -180
  148. data/lib/pact/provider/state/provider_state_configured_modules.rb +0 -15
  149. data/lib/pact/provider/state/provider_state_manager.rb +0 -42
  150. data/lib/pact/provider/state/provider_state_proxy.rb +0 -39
  151. data/lib/pact/provider/state/set_up.rb +0 -13
  152. data/lib/pact/provider/state/tear_down.rb +0 -13
  153. data/lib/pact/provider/test_methods.rb +0 -77
  154. data/lib/pact/provider/verification_report.rb +0 -36
  155. data/lib/pact/provider/verification_results/create.rb +0 -88
  156. data/lib/pact/provider/verification_results/publish.rb +0 -143
  157. data/lib/pact/provider/verification_results/publish_all.rb +0 -50
  158. data/lib/pact/provider/verification_results/verification_result.rb +0 -40
  159. data/lib/pact/provider/world.rb +0 -50
  160. data/lib/pact/retry.rb +0 -37
  161. data/lib/pact/tasks/task_helper.rb +0 -62
  162. data/lib/pact/tasks/verification_task.rb +0 -95
  163. data/lib/pact/tasks.rb +0 -2
  164. data/lib/pact/templates/help.erb +0 -22
  165. data/lib/pact/templates/provider_state.erb +0 -14
  166. data/lib/pact/utils/metrics.rb +0 -100
  167. data/lib/pact/utils/string.rb +0 -35
  168. data/lib/pact/v2/configuration.rb +0 -23
  169. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +0 -194
  170. data/lib/pact/v2/consumer/http_interaction_builder.rb +0 -162
  171. data/lib/pact/v2/consumer/interaction_contents.rb +0 -62
  172. data/lib/pact/v2/consumer/message_interaction_builder.rb +0 -288
  173. data/lib/pact/v2/consumer/mock_server.rb +0 -108
  174. data/lib/pact/v2/consumer/pact_config/base.rb +0 -24
  175. data/lib/pact/v2/consumer/pact_config/grpc.rb +0 -26
  176. data/lib/pact/v2/consumer/pact_config/http.rb +0 -55
  177. data/lib/pact/v2/consumer/pact_config/message.rb +0 -17
  178. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +0 -26
  179. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +0 -26
  180. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +0 -26
  181. data/lib/pact/v2/consumer/pact_config.rb +0 -30
  182. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +0 -171
  183. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +0 -201
  184. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +0 -180
  185. data/lib/pact/v2/consumer.rb +0 -8
  186. data/lib/pact/v2/generators/base.rb +0 -287
  187. data/lib/pact/v2/generators.rb +0 -49
  188. data/lib/pact/v2/matchers/base.rb +0 -74
  189. data/lib/pact/v2/matchers/v1/equality.rb +0 -19
  190. data/lib/pact/v2/matchers/v2/regex.rb +0 -19
  191. data/lib/pact/v2/matchers/v2/type.rb +0 -17
  192. data/lib/pact/v2/matchers/v3/boolean.rb +0 -17
  193. data/lib/pact/v2/matchers/v3/content_type.rb +0 -32
  194. data/lib/pact/v2/matchers/v3/date.rb +0 -18
  195. data/lib/pact/v2/matchers/v3/date_time.rb +0 -18
  196. data/lib/pact/v2/matchers/v3/decimal.rb +0 -17
  197. data/lib/pact/v2/matchers/v3/each.rb +0 -42
  198. data/lib/pact/v2/matchers/v3/include.rb +0 -17
  199. data/lib/pact/v2/matchers/v3/integer.rb +0 -17
  200. data/lib/pact/v2/matchers/v3/null.rb +0 -16
  201. data/lib/pact/v2/matchers/v3/number.rb +0 -17
  202. data/lib/pact/v2/matchers/v3/semver.rb +0 -23
  203. data/lib/pact/v2/matchers/v3/time.rb +0 -18
  204. data/lib/pact/v2/matchers/v3/values.rb +0 -16
  205. data/lib/pact/v2/matchers/v4/each_key.rb +0 -26
  206. data/lib/pact/v2/matchers/v4/each_key_value.rb +0 -32
  207. data/lib/pact/v2/matchers/v4/each_value.rb +0 -33
  208. data/lib/pact/v2/matchers/v4/not_empty.rb +0 -24
  209. data/lib/pact/v2/matchers/v4/status_code.rb +0 -17
  210. data/lib/pact/v2/matchers.rb +0 -110
  211. data/lib/pact/v2/native/blocking_verifier.rb +0 -17
  212. data/lib/pact/v2/native/logger.rb +0 -25
  213. data/lib/pact/v2/provider/async_message_verifier.rb +0 -28
  214. data/lib/pact/v2/provider/base_verifier.rb +0 -242
  215. data/lib/pact/v2/provider/grpc_verifier.rb +0 -38
  216. data/lib/pact/v2/provider/gruf_server.rb +0 -75
  217. data/lib/pact/v2/provider/http_server.rb +0 -79
  218. data/lib/pact/v2/provider/http_verifier.rb +0 -43
  219. data/lib/pact/v2/provider/message_provider_servlet.rb +0 -79
  220. data/lib/pact/v2/provider/mixed_verifier.rb +0 -22
  221. data/lib/pact/v2/provider/pact_broker_proxy.rb +0 -66
  222. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +0 -80
  223. data/lib/pact/v2/provider/pact_config/async.rb +0 -29
  224. data/lib/pact/v2/provider/pact_config/base.rb +0 -103
  225. data/lib/pact/v2/provider/pact_config/grpc.rb +0 -26
  226. data/lib/pact/v2/provider/pact_config/http.rb +0 -27
  227. data/lib/pact/v2/provider/pact_config/mixed.rb +0 -40
  228. data/lib/pact/v2/provider/pact_config.rb +0 -26
  229. data/lib/pact/v2/provider/provider_server_runner.rb +0 -92
  230. data/lib/pact/v2/provider/provider_state_configuration.rb +0 -32
  231. data/lib/pact/v2/provider/provider_state_servlet.rb +0 -86
  232. data/lib/pact/v2/provider.rb +0 -8
  233. data/lib/pact/v2/railtie.rb +0 -13
  234. data/lib/pact/v2/tasks/pact.rake +0 -13
  235. data/lib/pact/v2.rb +0 -71
  236. data/lib/tasks/pact.rake +0 -34
  237. /data/lib/pact/{v2/rspec → rspec}/support/pact_message_helpers.rb +0 -0
  238. /data/lib/pact/{v2/rspec → rspec}/support/waterdrop/pact_waterdrop_client.rb +0 -0
  239. /data/lib/pact/{v2/rspec → rspec}/support/webmock/webmock_helpers.rb +0 -0
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+
7
+ module Pact
8
+ module Consumer
9
+ class MessageInteractionBuilder
10
+ META_CONTENT_TYPE_HEADER = "contentType"
11
+
12
+ JSON_CONTENT_TYPE = "application/json"
13
+ PROTO_CONTENT_TYPE = "application/protobuf"
14
+
15
+ PROTOBUF_PLUGIN_NAME = "protobuf"
16
+ PROTOBUF_PLUGIN_VERSION = "0.6.5"
17
+
18
+ # https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/handles/fn.pactffi_write_message_pact_file.html
19
+ WRITE_PACT_FILE_ERRORS = {
20
+ 1 => {reason: :file_not_accessible, status: 1, description: "The pact file was not able to be written"},
21
+ 2 => {reason: :internal_error, status: 2, description: "The message pact for the given handle was not found"}
22
+ }.freeze
23
+
24
+ class PluginInitError < Pact::FfiError; end
25
+
26
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
27
+ INIT_PLUGIN_ERRORS = {
28
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
29
+ 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
30
+ 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
31
+ }.freeze
32
+
33
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
34
+ CREATE_INTERACTION_ERRORS = {
35
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
36
+ 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
37
+ 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
38
+ 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
39
+ 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
40
+ 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
41
+ }.freeze
42
+
43
+ class CreateInteractionError < Pact::FfiError; end
44
+
45
+ class InteractionMismatchesError < Pact::Error; end
46
+
47
+ class InteractionBuilderError < Pact::Error; end
48
+
49
+ def initialize(pact_config, description: nil)
50
+ @pact_config = pact_config
51
+ @description = description
52
+
53
+ @json_contents = nil
54
+ @proto_contents = nil
55
+ @proto_path = nil
56
+ @proto_message_class = nil
57
+ @proto_include_dirs = []
58
+ @meta = {}
59
+ @headers = {}
60
+ @provider_state_meta = nil
61
+ end
62
+
63
+ def given(provider_state, metadata = {})
64
+ @provider_state_meta = {provider_state => metadata}
65
+ self
66
+ end
67
+
68
+ def upon_receiving(description)
69
+ @description = description
70
+ self
71
+ end
72
+
73
+ def with_json_contents(contents_hash)
74
+ @json_contents = InteractionContents.basic(contents_hash)
75
+ self
76
+ end
77
+
78
+ def with_proto_class(proto_path, message_class_name, include_dirs = [])
79
+ absolute_path = File.expand_path(proto_path)
80
+ raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path)
81
+
82
+ @proto_path = absolute_path
83
+ @proto_message_class = message_class_name
84
+ @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) }
85
+ self
86
+ end
87
+
88
+ def with_pact_protobuf_plugin_version(version)
89
+ raise InteractionBuilderError.new("version is required") if version.blank?
90
+
91
+ @proto_plugin_version = version
92
+ self
93
+ end
94
+
95
+ def with_proto_contents(contents_hash)
96
+ @proto_contents = InteractionContents.plugin(contents_hash)
97
+ self
98
+ end
99
+
100
+ def with_metadata(meta_hash)
101
+ @meta = InteractionContents.basic(meta_hash)
102
+ self
103
+ end
104
+
105
+ def with_headers(headers_hash)
106
+ @headers = InteractionContents.basic(headers_hash)
107
+ self
108
+ end
109
+
110
+ def with_header(key, value)
111
+ @headers[key] = value
112
+ self
113
+ end
114
+
115
+ def validate!
116
+ if proto_interaction?
117
+ raise InteractionBuilderError.new("proto_path / proto_message are not defined, please set ones with #with_proto_message") if @proto_contents.blank? || @proto_message_class.blank?
118
+ raise InteractionBuilderError.new("invalid request format, should be a hash") unless @proto_contents.is_a?(Hash)
119
+ else
120
+ raise InteractionBuilderError.new("invalid request format, should be a hash") unless @json_contents.is_a?(Hash)
121
+ end
122
+ raise InteractionBuilderError.new("description is required for message interactions, please set one with #upon_receiving") if @description.blank?
123
+ end
124
+
125
+ def execute(&block)
126
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
127
+
128
+ validate!
129
+ pact_handle = init_pact
130
+ init_plugin!(pact_handle) if proto_interaction?
131
+
132
+ message_pact = PactFfi::MessageConsumer.new_message_interaction(pact_handle, @description)
133
+
134
+ configure_interaction!(message_pact)
135
+
136
+ # strip out matchers and get raw payload/metadata
137
+ payload, metadata = fetch_reified_message(pact_handle)
138
+ configure_provider_state(message_pact, metadata)
139
+
140
+ yield(payload, metadata)
141
+
142
+ write_pacts!(pact_handle, @pact_config.pact_dir)
143
+ ensure
144
+ @used = true
145
+ PactFfi::MessageConsumer.free_handle(message_pact)
146
+ PactFfi::PluginConsumer.cleanup_plugins(pact_handle)
147
+ PactFfi.free_pact_handle(pact_handle)
148
+ end
149
+
150
+ def build_interaction_json
151
+ return JSON.dump(@json_contents) unless proto_interaction?
152
+
153
+ contents = {
154
+ "pact:proto": @proto_path,
155
+ "pact:message-type": @proto_message_class,
156
+ "pact:content-type": PROTO_CONTENT_TYPE
157
+ }.merge(@proto_contents)
158
+
159
+ contents["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present?
160
+
161
+ JSON.dump(contents)
162
+ end
163
+
164
+ private
165
+
166
+ def write_pacts!(handle, dir)
167
+ result = PactFfi.write_message_pact_file(handle, @pact_config.pact_dir, false)
168
+ return result if WRITE_PACT_FILE_ERRORS[result].blank?
169
+
170
+ error = WRITE_PACT_FILE_ERRORS[result]
171
+ raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status])
172
+ end
173
+
174
+ def init_pact
175
+ handle = PactFfi::MessageConsumer.new_message_pact(@pact_config.consumer_name, @pact_config.provider_name)
176
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
177
+ PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
178
+
179
+ Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
180
+
181
+ handle
182
+ end
183
+
184
+ def fetch_reified_message(pact_handle)
185
+ iterator = PactFfi::MessageConsumer.pact_handle_get_message_iter(pact_handle)
186
+ raise InteractionBuilderError.new("cannot get message iterator: internal error") if iterator.blank?
187
+
188
+ message_handle = PactFfi.pact_message_iter_next(iterator)
189
+ raise InteractionBuilderError.new("cannot get message from iterator: no messages") if message_handle.blank?
190
+
191
+ contents = fetch_reified_message_body(message_handle)
192
+ meta = fetch_reified_message_headers(message_handle)
193
+
194
+ [contents, meta.compact]
195
+ ensure
196
+ PactFfi.pact_message_iter_delete(iterator) if iterator.present?
197
+ end
198
+
199
+ def fetch_reified_message_headers(message_handle)
200
+ meta = {"headers" => {}}
201
+
202
+ meta[META_CONTENT_TYPE_HEADER] = PactFfi.message_find_metadata(message_handle, META_CONTENT_TYPE_HEADER)
203
+
204
+ @meta.each_key do |key|
205
+ meta[key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s)
206
+ end
207
+
208
+ @headers.each_key do |key|
209
+ meta["headers"][key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s)
210
+ end
211
+
212
+ meta
213
+ end
214
+
215
+ def configure_provider_state(message_pact, reified_metadata)
216
+ content_type = reified_metadata[META_CONTENT_TYPE_HEADER]
217
+ @provider_state_meta&.each_pair do |provider_state, meta|
218
+ if meta.present?
219
+ meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) }
220
+ PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) if content_type
221
+ elsif content_type.present?
222
+ PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s)
223
+ else
224
+ PactFfi.given(message_pact, provider_state)
225
+ end
226
+ end
227
+ end
228
+
229
+ def fetch_reified_message_body(message_handle)
230
+ if proto_interaction?
231
+ len = PactFfi::MessageConsumer.get_contents_length(message_handle)
232
+ ptr = PactFfi::MessageConsumer.get_contents_bin(message_handle)
233
+ return nil if ptr.blank? || len == 0
234
+
235
+ return String.new(ptr.read_string_length(len))
236
+ end
237
+
238
+ contents = PactFfi::MessageConsumer.get_contents(message_handle)
239
+ return nil if contents.blank?
240
+
241
+ JSON.parse(contents)
242
+ end
243
+
244
+ def configure_interaction!(message_pact)
245
+ interaction_json = build_interaction_json
246
+
247
+ if proto_interaction?
248
+ result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, PROTO_CONTENT_TYPE, interaction_json)
249
+ if CREATE_INTERACTION_ERRORS[result].present?
250
+ error = CREATE_INTERACTION_ERRORS[result]
251
+ raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
252
+ end
253
+ else
254
+ result = PactFfi.with_body(message_pact, 0, JSON_CONTENT_TYPE, interaction_json)
255
+ unless result
256
+ raise InteractionMismatchesError.new("There was an error while trying to add message interaction contents \"#{@description}\"")
257
+ end
258
+ end
259
+
260
+ # meta should be configured last to avoid resetting after body is set
261
+ InteractionContents.basic(@meta.merge(@headers)).each_pair do |key, value|
262
+ PactFfi::MessageConsumer.with_metadata_v2(message_pact, key.to_s, JSON.dump(value))
263
+ end
264
+ end
265
+
266
+ def init_plugin!(pact_handle)
267
+ result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION)
268
+ return result if INIT_PLUGIN_ERRORS[result].blank?
269
+
270
+ error = INIT_PLUGIN_ERRORS[result]
271
+ 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])
272
+ end
273
+
274
+ def serialize_metadata(metadata_hash)
275
+ metadata = metadata_hash.deep_dup
276
+ serialize_as!(metadata, :basic)
277
+
278
+ metadata
279
+ end
280
+
281
+ def proto_interaction?
282
+ @proto_contents.present?
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/mock_server"
4
+
5
+ module Pact
6
+ module Consumer
7
+ class MockServer
8
+ attr_reader :host, :port, :transport, :handle, :url
9
+
10
+ TRANSPORT_HTTP = "http"
11
+ TRANSPORT_GRPC = "grpc"
12
+
13
+ class MockServerCreateError < Pact::FfiError; end
14
+
15
+ class WritePactsError < Pact::FfiError; end
16
+
17
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html
18
+ CREATE_TRANSPORT_ERRORS = {
19
+ -1 => {reason: :invalid_handle, status: -1, description: "An invalid handle was received. Handles should be created with pactffi_new_pact"},
20
+ -2 => {reason: :invalid_transport_json, status: -2, description: "Transport_config is not valid JSON"},
21
+ -3 => {reason: :mock_server_not_started, status: -3, description: "The mock server could not be started"},
22
+ -4 => {reason: :internal_error, status: -4, description: "The method panicked"},
23
+ -5 => {reason: :invalid_host, status: -5, description: "The address is not valid"}
24
+ }.freeze
25
+
26
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_write_pact_file.html
27
+ WRITE_PACT_FILE_ERRORS = {
28
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
29
+ 2 => {reason: :file_not_accessible, status: 2, description: "The pact file was not able to be written"},
30
+ 3 => {reason: :mock_server_not_found, status: 3, description: "A mock server with the provided port was not found"}
31
+ }.freeze
32
+
33
+ def self.create_for_grpc!(pact:, host: "127.0.0.1", port: 0)
34
+ new(pact: pact, transport: TRANSPORT_GRPC, host: host, port: port)
35
+ end
36
+
37
+ def self.create_for_http!(pact:, host: "127.0.0.1", port: 0)
38
+ new(pact: pact, transport: TRANSPORT_HTTP, host: host, port: port)
39
+ end
40
+
41
+ def self.create_for_transport!(pact:, transport:, host: "127.0.0.1", port: 0)
42
+ new(pact: pact, transport: transport, host: host, port: port)
43
+ end
44
+
45
+ def initialize(pact:, transport:, host:, port:)
46
+
47
+ @pact = pact
48
+ @transport = transport
49
+ @host = host
50
+ @port = port
51
+
52
+ @handle = init_transport!
53
+ # the returned handle is the port number
54
+ # we set it here, so we can consume a port number of 0
55
+ # and allow pact to assign a random available port
56
+ @port = @handle
57
+ # construct the url for the mock server
58
+ # as a convenience for the user
59
+ @url = "#{transport}://#{host}:#{@handle}"
60
+ # TODO: handle auto-GC of native memory
61
+ # ObjectSpace.define_finalizer(self, proc do
62
+ # cleanup
63
+ # end)
64
+ end
65
+
66
+ def write_pacts!(dir)
67
+ result = PactFfi::MockServer.write_pact_file(@handle, dir, false)
68
+ return result if WRITE_PACT_FILE_ERRORS[result].blank?
69
+
70
+ error = WRITE_PACT_FILE_ERRORS[result]
71
+ raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status])
72
+ end
73
+
74
+ def matched?
75
+ PactFfi::MockServer.matched(@handle)
76
+ end
77
+
78
+ def mismatches
79
+ PactFfi::MockServer.mismatches(@handle)
80
+ end
81
+
82
+ def cleanup
83
+ PactFfi::MockServer.cleanup(@handle)
84
+ end
85
+
86
+ def cleanup_plugins
87
+ PactFfi::PluginConsumer.cleanup_plugins(@handle)
88
+ end
89
+
90
+ def free_pact_handle
91
+ PactFfi.free_pact_handle(@handle)
92
+ end
93
+
94
+ private
95
+
96
+ def init_transport!
97
+ handle = PactFfi::MockServer.create_for_transport(@pact, @host, @port, @transport, nil)
98
+ # the returned handle is the port number
99
+ return handle if CREATE_TRANSPORT_ERRORS[handle].blank?
100
+
101
+ error = CREATE_TRANSPORT_ERRORS[handle]
102
+ raise MockServerCreateError.new("There was an error while trying to create mock server for transport:#{@transport}", error[:reason], error[:status])
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module Consumer
5
+ module PactConfig
6
+ class Base
7
+ attr_reader :consumer_name, :provider_name, :pact_dir, :log_level
8
+
9
+ def initialize(consumer_name:, provider_name:, opts: {})
10
+ @consumer_name = consumer_name
11
+ @provider_name = provider_name
12
+ @pact_dir = opts[:pact_dir] || (defined?(Rails) ? Rails.root.join("../pacts").to_s : "pacts")
13
+ @log_level = opts[:log_level] || :info
14
+ end
15
+
16
+ def new_interaction(description = nil)
17
+ raise PactImplementationRequired, "#new_interaction should be implemented"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class Grpc < Base
9
+ attr_reader :mock_host, :mock_port
10
+
11
+ def initialize(consumer_name:, provider_name:, opts: {})
12
+ super
13
+
14
+ @mock_host = opts[:mock_host] || "127.0.0.1"
15
+ @mock_port = opts[:mock_port] || 3009
16
+ end
17
+
18
+ def new_interaction(description = nil)
19
+ GrpcInteractionBuilder.new(self, description: description)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class Http < Base
9
+ attr_reader :mock_host, :mock_port, :pact_handle
10
+
11
+ def initialize(consumer_name:, provider_name:, opts: {})
12
+ super
13
+
14
+ @mock_host = opts[:mock_host] || "127.0.0.1"
15
+ @mock_port = opts[:mock_port] || 0
16
+ @log_level = opts[:log_level] || :info
17
+ @pact_specification = get_pact_specification(opts)
18
+ @pact_handle = init_pact
19
+ end
20
+
21
+ def new_interaction(description = nil)
22
+ HttpInteractionBuilder.new(self, description: description)
23
+ end
24
+
25
+ def reset_pact
26
+ @pact_handle = init_pact
27
+ end
28
+
29
+ def get_pact_specification(opts)
30
+ pact_spec_version = opts[:pact_specification] || "V4"
31
+ unless pact_spec_version.match?(/^v?[1-4](\.\d+){0,2}$/i)
32
+ raise ArgumentError, "Invalid pact specification version format \n Valid versions are 1, 1.1, 2, 3, 4. Default is V4 \n V prefix is optional, and case insensitive"
33
+ end
34
+ pact_spec_version = pact_spec_version.upcase
35
+ pact_spec_version = "V#{pact_spec_version}" unless pact_spec_version.start_with?("V")
36
+ pact_spec_version = pact_spec_version.sub(/(\.0+)+$/, "")
37
+ pact_spec_version = pact_spec_version.tr(".", "_")
38
+ PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_spec_version.upcase}"]
39
+ end
40
+
41
+ def init_pact
42
+ handle = PactFfi.new_pact(consumer_name, provider_name)
43
+ PactFfi.with_specification(handle, @pact_specification)
44
+ PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version)
45
+
46
+ Pact::Native::Logger.log_to_stdout(@log_level)
47
+
48
+ handle
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class Message < Base
9
+ def new_interaction(description = nil)
10
+ MessageInteractionBuilder.new(self, description: description)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class PluginAsyncMessage < Base
9
+ attr_reader :mock_host, :mock_port
10
+
11
+ def initialize(consumer_name:, provider_name:, opts: {})
12
+ super
13
+
14
+ @mock_host = opts[:mock_host] || "127.0.0.1"
15
+ @mock_port = opts[:mock_port] || 0
16
+ end
17
+
18
+ def new_interaction(description = nil)
19
+ PluginAsyncMessageInteractionBuilder.new(self, description: description)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class PluginHttp < Base
9
+ attr_reader :mock_host, :mock_port
10
+
11
+ def initialize(consumer_name:, provider_name:, opts: {})
12
+ super
13
+
14
+ @mock_host = opts[:mock_host] || "127.0.0.1"
15
+ @mock_port = opts[:mock_port] || 0
16
+ end
17
+
18
+ def new_interaction(description = nil)
19
+ PluginHttpInteractionBuilder.new(self, description: description)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ class PluginSyncMessage < Base
9
+ attr_reader :mock_host, :mock_port
10
+
11
+ def initialize(consumer_name:, provider_name:, opts: {})
12
+ super
13
+
14
+ @mock_host = opts[:mock_host] || "127.0.0.1"
15
+ @mock_port = opts[:mock_port] || 0
16
+ end
17
+
18
+ def new_interaction(description = nil)
19
+ PluginSyncMessageInteractionBuilder.new(self, description: description)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pact_config/grpc"
4
+
5
+ module Pact
6
+ module Consumer
7
+ module PactConfig
8
+ def self.new(transport_type, consumer_name:, provider_name:, opts: {})
9
+ case transport_type
10
+ when :http
11
+ Http.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
12
+ when :grpc
13
+ Grpc.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
14
+ when :message
15
+ Message.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
16
+ when :plugin_sync_message
17
+ PluginSyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
18
+ when :plugin_async_message
19
+ PluginAsyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
20
+ when :plugin_http
21
+ PluginHttp.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
22
+ else
23
+ raise ArgumentError, "unknown transport_type: #{transport_type}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end