pact-v2 2.0.0.pre.preview1

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