pact 1.66.2 → 1.67.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/pact/v2/configuration.rb +23 -0
  4. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +194 -0
  5. data/lib/pact/v2/consumer/http_interaction_builder.rb +162 -0
  6. data/lib/pact/v2/consumer/interaction_contents.rb +62 -0
  7. data/lib/pact/v2/consumer/message_interaction_builder.rb +287 -0
  8. data/lib/pact/v2/consumer/mock_server.rb +100 -0
  9. data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
  10. data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
  11. data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
  12. data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
  13. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +26 -0
  14. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +26 -0
  15. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +26 -0
  16. data/lib/pact/v2/consumer/pact_config.rb +30 -0
  17. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +171 -0
  18. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +201 -0
  19. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +180 -0
  20. data/lib/pact/v2/consumer.rb +8 -0
  21. data/lib/pact/v2/generators/base.rb +287 -0
  22. data/lib/pact/v2/generators.rb +49 -0
  23. data/lib/pact/v2/matchers/base.rb +74 -0
  24. data/lib/pact/v2/matchers/v1/equality.rb +19 -0
  25. data/lib/pact/v2/matchers/v2/regex.rb +19 -0
  26. data/lib/pact/v2/matchers/v2/type.rb +17 -0
  27. data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
  28. data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
  29. data/lib/pact/v2/matchers/v3/date.rb +18 -0
  30. data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
  31. data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
  32. data/lib/pact/v2/matchers/v3/each.rb +42 -0
  33. data/lib/pact/v2/matchers/v3/include.rb +17 -0
  34. data/lib/pact/v2/matchers/v3/integer.rb +17 -0
  35. data/lib/pact/v2/matchers/v3/null.rb +16 -0
  36. data/lib/pact/v2/matchers/v3/number.rb +17 -0
  37. data/lib/pact/v2/matchers/v3/semver.rb +23 -0
  38. data/lib/pact/v2/matchers/v3/time.rb +18 -0
  39. data/lib/pact/v2/matchers/v3/values.rb +16 -0
  40. data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
  41. data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
  42. data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
  43. data/lib/pact/v2/matchers/v4/not_empty.rb +24 -0
  44. data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
  45. data/lib/pact/v2/matchers.rb +110 -0
  46. data/lib/pact/v2/native/blocking_verifier.rb +17 -0
  47. data/lib/pact/v2/native/logger.rb +25 -0
  48. data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
  49. data/lib/pact/v2/provider/base_verifier.rb +242 -0
  50. data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
  51. data/lib/pact/v2/provider/gruf_server.rb +75 -0
  52. data/lib/pact/v2/provider/http_server.rb +79 -0
  53. data/lib/pact/v2/provider/http_verifier.rb +43 -0
  54. data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
  55. data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
  56. data/lib/pact/v2/provider/pact_broker_proxy.rb +66 -0
  57. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
  58. data/lib/pact/v2/provider/pact_config/async.rb +29 -0
  59. data/lib/pact/v2/provider/pact_config/base.rb +101 -0
  60. data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
  61. data/lib/pact/v2/provider/pact_config/http.rb +27 -0
  62. data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
  63. data/lib/pact/v2/provider/pact_config.rb +26 -0
  64. data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
  65. data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
  66. data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
  67. data/lib/pact/v2/provider.rb +8 -0
  68. data/lib/pact/v2/railtie.rb +13 -0
  69. data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +93 -0
  70. data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
  71. data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
  72. data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
  73. data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
  74. data/lib/pact/v2/rspec.rb +17 -0
  75. data/lib/pact/v2/tasks/pact.rake +13 -0
  76. data/lib/pact/v2.rb +71 -0
  77. data/lib/pact/version.rb +1 -1
  78. data/lib/pact.rb +1 -0
  79. data/pact.gemspec +52 -7
  80. metadata +419 -65
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V3
7
+ class Semver < Pact::V2::Matchers::Base
8
+ def initialize(template = nil)
9
+ @template = template
10
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "semver", template: template)
11
+ end
12
+
13
+ def as_plugin
14
+ if @template.nil? || @template.blank?
15
+ raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin"
16
+ end
17
+ "matching(semver, '#{@template}')"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V3
7
+ class Time < Pact::V2::Matchers::Base
8
+ def initialize(format, template)
9
+ raise MatcherInitializationError, "#{self.class}: #{format} should be an instance of String" unless template.is_a?(String)
10
+ raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String" unless template.is_a?(String)
11
+
12
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "time", template: template, opts: {format: format})
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V3
7
+ class Values < Pact::V2::Matchers::Base
8
+ def initialize
9
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "values", template: nil)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V4
7
+ class EachKey < Pact::V2::Matchers::Base
8
+ def initialize(key_matchers, template)
9
+ raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash)
10
+ raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be an Array" unless key_matchers.is_a?(Array)
11
+ raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be instances of Pact::V2::Matchers::Base" unless key_matchers.all?(Pact::V2::Matchers::Base)
12
+ raise MatcherInitializationError, "#{self.class}: #{key_matchers} size should be greater than 0" unless key_matchers.size > 0
13
+
14
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: "each-key", template: template, opts: {rules: key_matchers})
15
+ end
16
+
17
+ def as_plugin
18
+ @opts[:rules].map do |matcher|
19
+ "eachKey(#{matcher.as_plugin})"
20
+ end.join(", ")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V4
7
+ class EachKeyValue < Pact::V2::Matchers::Base
8
+ def initialize(key_matchers, template)
9
+ raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash)
10
+ raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be an Array" unless key_matchers.is_a?(Array)
11
+ raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be instances of Pact::V2::Matchers::Base" unless key_matchers.all?(Pact::V2::Matchers::Base)
12
+
13
+ super(
14
+ spec_version: Pact::V2::Matchers::PACT_SPEC_V4,
15
+ kind: [
16
+ EachKey.new(key_matchers, {}),
17
+ EachValue.new([Pact::V2::Matchers::V2::Type.new("")], {})
18
+ ],
19
+ template: template
20
+ )
21
+
22
+ @key_matchers = key_matchers
23
+ end
24
+
25
+ def as_plugin
26
+ raise MatcherInitializationError, "#{self.class}: each-key-value is not supported in plugin syntax. Use each / each_key / each_value matchers instead"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V4
7
+ class EachValue < Pact::V2::Matchers::Base
8
+ def initialize(value_matchers, template)
9
+ # raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash)
10
+ raise MatcherInitializationError, "#{self.class}: #{value_matchers} should be an Array" unless value_matchers.is_a?(Array)
11
+ raise MatcherInitializationError, "#{self.class}: #{value_matchers} should be instances of Pact::V2::Matchers::Base" unless value_matchers.all?(Pact::V2::Matchers::Base)
12
+ raise MatcherInitializationError, "#{self.class}: #{value_matchers} size should be greater than 0" unless value_matchers.size > 0
13
+
14
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: "each-value", template: template, opts: {rules: value_matchers})
15
+ end
16
+
17
+ def as_plugin
18
+ if @template.is_a?(Hash)
19
+ return {
20
+ "pact:match" => "eachValue(matching($'SAMPLE'))",
21
+ "SAMPLE" => serialize!(@template.deep_dup, :plugin)
22
+ }
23
+ end
24
+
25
+ @opts[:rules].map do |matcher|
26
+ "eachValue(#{matcher.as_plugin})"
27
+ end.join(", ")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V4
7
+ class NotEmpty < Pact::V2::Matchers::Base
8
+ def initialize(template = nil)
9
+ @template = template
10
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: 'notEmpty', template: @template)
11
+ end
12
+
13
+ def as_plugin
14
+ if @template.nil? || @template.blank?
15
+ raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin"
16
+ end
17
+
18
+ "notEmpty('#{@template}')"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ module V4
7
+ class StatusCode < Pact::V2::Matchers::Base
8
+ def initialize(template = nil)
9
+ super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: 'statusCode', opts: {
10
+ 'status' => template
11
+ })
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Matchers
6
+ PACT_SPEC_V1 = 1
7
+ PACT_SPEC_V2 = 2
8
+ PACT_SPEC_V3 = 3
9
+ PACT_SPEC_V4 = 4
10
+
11
+ ANY_STRING_REGEX = /.*/
12
+ UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
13
+
14
+ # simplified
15
+ ISO8601_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)*(.\d{2}:\d{2})*/i
16
+
17
+ def match_exactly(arg)
18
+ V1::Equality.new(arg)
19
+ end
20
+
21
+ def match_type_of(arg)
22
+ V2::Type.new(arg)
23
+ end
24
+
25
+ def match_include(arg)
26
+ V3::Include.new(arg)
27
+ end
28
+
29
+ def match_any_string(sample = "any")
30
+ V2::Regex.new(ANY_STRING_REGEX, sample)
31
+ end
32
+
33
+ def match_any_integer(sample = 10)
34
+ V3::Integer.new(sample)
35
+ end
36
+
37
+ def match_any_decimal(sample = 10.0)
38
+ V3::Decimal.new(sample)
39
+ end
40
+
41
+ def match_any_number(sample = 10.0)
42
+ V3::Number.new(sample)
43
+ end
44
+
45
+ def match_any_boolean(sample = true)
46
+ V3::Boolean.new(sample)
47
+ end
48
+
49
+ def match_uuid(sample = "e1d01e04-3a2b-4eed-a4fb-54f5cd257338")
50
+ V2::Regex.new(UUID_REGEX, sample)
51
+ end
52
+
53
+ def match_regex(regex, sample)
54
+ V2::Regex.new(regex, sample)
55
+ end
56
+
57
+ def match_datetime(format, sample)
58
+ V3::DateTime.new(format, sample)
59
+ end
60
+
61
+ def match_iso8601(sample = "2024-08-12T12:25:00.243118+03:00")
62
+ V2::Regex.new(ISO8601_REGEX, sample)
63
+ end
64
+
65
+ def match_date(format, sample)
66
+ V3::Date.new(format, sample)
67
+ end
68
+
69
+ def match_time(format, sample)
70
+ V3::Time.new(format, sample)
71
+ end
72
+
73
+ def match_each(template, min = nil)
74
+ V3::Each.new(template, min)
75
+ end
76
+
77
+ def match_each_regex(regex, sample)
78
+ match_each_value(sample, match_regex(regex, sample))
79
+ end
80
+
81
+ def match_each_key(template, key_matchers)
82
+ V4::EachKey.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template)
83
+ end
84
+
85
+ def match_each_value(template, value_matchers = V2::Type.new(""))
86
+ V4::EachValue.new(value_matchers.is_a?(Array) ? value_matchers : [value_matchers], template)
87
+ end
88
+
89
+ def match_each_kv(template, key_matchers)
90
+ V4::EachKeyValue.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template)
91
+ end
92
+
93
+ def match_semver(template = nil)
94
+ V3::Semver.new(template)
95
+ end
96
+
97
+ def match_content_type(content_type, template = nil)
98
+ V3::ContentType.new(content_type, template: template)
99
+ end
100
+
101
+ def match_not_empty(template = nil)
102
+ V4::NotEmpty.new(template)
103
+ end
104
+
105
+ def match_status_code(template)
106
+ V4::StatusCode.new(template)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "pact/ffi/verifier"
5
+
6
+ module Pact
7
+ module V2
8
+ module Native
9
+ module BlockingVerifier
10
+ extend FFI::Library
11
+ ffi_lib DetectOS.get_bin_path
12
+
13
+ attach_function :execute, :pactffi_verifier_execute, %i[pointer], :int32, blocking: true
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/logger"
4
+
5
+ module Pact
6
+ module V2
7
+ module Native
8
+ module Logger
9
+ LOG_LEVELS = {
10
+ off: PactFfi::FfiLogLevelFilter["LOG_LEVEL_OFF"],
11
+ error: PactFfi::FfiLogLevelFilter["LOG_LEVEL_ERROR"],
12
+ warn: PactFfi::FfiLogLevelFilter["LOG_LEVEL_WARN"],
13
+ info: PactFfi::FfiLogLevelFilter["LOG_LEVEL_INFO"],
14
+ debug: PactFfi::FfiLogLevelFilter["LOG_LEVEL_DEBUG"],
15
+ trace: PactFfi::FfiLogLevelFilter["LOG_LEVEL_TRACE"]
16
+ }.freeze
17
+
18
+ def self.log_to_stdout(log_level)
19
+ raise "invalid log level for PactFfi::FfiLogLevelFilter" unless LOG_LEVELS.key?(log_level)
20
+ PactFfi::Logger.log_to_stdout(LOG_LEVELS[log_level])
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+
6
+ module Pact
7
+ module V2
8
+ module Provider
9
+ class AsyncMessageVerifier < BaseVerifier
10
+ PROVIDER_TRANSPORT_TYPE = "message"
11
+
12
+ def initialize(pact_config, mixed_config = nil)
13
+ super
14
+
15
+ raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Message" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Async)
16
+ end
17
+
18
+ private
19
+
20
+ def add_provider_transport(pact_handle)
21
+ setup_uri = URI(@pact_config.message_setup_url)
22
+ PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, setup_uri.port, setup_uri.path, "")
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+ require "pact/v2/native/blocking_verifier"
6
+
7
+ module Pact
8
+ module V2
9
+ module Provider
10
+ class BaseVerifier
11
+ PROVIDER_TRANSPORT_TYPE = nil
12
+ attr_reader :logger
13
+
14
+ class VerificationError < Pact::V2::FfiError; end
15
+
16
+ class VerifierError < Pact::V2::Error; end
17
+
18
+ DEFAULT_CONSUMER_SELECTORS = {}
19
+
20
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/verifier/fn.pactffi_verify.html#errors
21
+ VERIFICATION_ERRORS = {
22
+ 1 => {reason: :verification_failed, status: 1, description: "The verification process failed, see output for errors"},
23
+ 2 => {reason: :null_pointer, status: 2, description: "A null pointer was received"},
24
+ 3 => {reason: :internal_error, status: 3, description: "The method panicked"},
25
+ 4 => {reason: :invalid_arguments, status: 4, description: "Invalid arguments were provided to the verification process"}
26
+ }.freeze
27
+
28
+ # env below are set up by pipeline-builder
29
+ # see paas/cicd/images/pact/pipeline-builder/-/blob/master/internal/commands/consumers-pipeline/ruby.go
30
+ def initialize(pact_config, mixed_config = nil)
31
+ raise ArgumentError, "pact_config must be a subclass of Pact::V2::Provider::PactConfig::Base" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Base)
32
+ @pact_config = pact_config
33
+ @mixed_config = mixed_config
34
+ @logger = Logger.new($stdout)
35
+ end
36
+
37
+ def verify!
38
+ raise VerifierError.new("interaction is designed to be used one-time only") if defined?(@used)
39
+
40
+ # if consumer_selectors.blank?
41
+ # logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}")
42
+ # return
43
+ # end
44
+
45
+ exception = nil
46
+ pact_handle = init_pact
47
+
48
+ start_servers!
49
+
50
+ logger.info("[verifier] starting provider verification")
51
+
52
+ result = Pact::V2::Native::BlockingVerifier.execute(pact_handle)
53
+ if VERIFICATION_ERRORS[result].present?
54
+ error = VERIFICATION_ERRORS[result]
55
+ exception = VerificationError.new("There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status])
56
+ end
57
+ ensure
58
+ @used = true
59
+ PactFfi::Verifier.shutdown(pact_handle) if pact_handle
60
+ stop_servers
61
+ @grpc_server.stop if @grpc_server
62
+ raise exception if exception
63
+ end
64
+
65
+ private
66
+
67
+ def create_c_pointer_array_from_string_array(string_array)
68
+ pointers = string_array.map { |str| FFI::MemoryPointer.from_string(str) }
69
+ array_pointer = FFI::MemoryPointer.new(:pointer, pointers.size)
70
+ pointers.each_with_index do |ptr, index|
71
+ array_pointer[index].put_pointer(0, ptr)
72
+ end
73
+ array_pointer
74
+ end
75
+
76
+ def bool_to_int(value)
77
+ value ? 1 : 0
78
+ end
79
+
80
+ def init_pact
81
+ handle = PactFfi::Verifier.new_for_application("pact-ruby-v2", PactFfi.version)
82
+ set_provider_info(handle)
83
+
84
+ if defined?(@mixed_config.grpc_config) && @mixed_config.grpc_config
85
+ @grpc_server = GrufServer.new(host: "127.0.0.1:#{@mixed_config.grpc_config.grpc_port}", services: @mixed_config.grpc_config.grpc_services)
86
+ @grpc_server.start
87
+ PactFfi::Verifier.add_provider_transport(handle, "grpc", @mixed_config.grpc_config.grpc_port, "", "")
88
+ end
89
+
90
+ if defined?(@mixed_config.async_config) && @mixed_config.async_config
91
+ setup_uri = URI(@mixed_config.async_config.message_setup_url)
92
+ PactFfi::Verifier.add_provider_transport(handle, "message", setup_uri.port, setup_uri.path, "")
93
+ end
94
+
95
+ # todo: add http transport?
96
+
97
+ PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1)
98
+ PactFfi::Verifier.set_verification_options(handle, 0, 10000)
99
+ # pactffi_verifier_set_publish_options(
100
+ # handle: *mut VerifierHandle,
101
+ # provider_version: *const c_char,
102
+ # build_url: *const c_char,
103
+ # provider_tags: *const *const c_char,
104
+ # provider_tags_len: c_ushort,
105
+ # provider_branch: *const c_char,
106
+ # )
107
+ c_provider_version_tags = create_c_pointer_array_from_string_array(@pact_config.provider_version_tags)
108
+ c_provider_version_tags_size = @pact_config.provider_version_tags.size
109
+ c_consumer_version_tags = create_c_pointer_array_from_string_array(@pact_config.consumer_version_tags)
110
+ c_consumer_version_tags_size = @pact_config.consumer_version_tags.size
111
+
112
+ if @pact_config.provider_build_uri.present?
113
+ begin
114
+ URI.parse(@pact_config.provider_build_uri)
115
+ rescue URI::InvalidURIError
116
+ raise VerifierError.new("provider_build_uri is not a valid URI")
117
+ end
118
+ end
119
+
120
+ if @pact_config.publish_verification_results == true
121
+ if @pact_config.provider_version
122
+ PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, @pact_config.provider_build_uri, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch)
123
+ else
124
+ logger.warn("[verifier] - unable to publish verification results as provider version is not set")
125
+ end
126
+ end
127
+
128
+ configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
129
+
130
+ PactFfi::Verifier.set_no_pacts_is_error(handle, bool_to_int(@pact_config.fail_if_no_pacts_found))
131
+
132
+ add_provider_transport(handle)
133
+
134
+ # the core doesnt pick up these env vars, so we need to set them here
135
+ # https://github.com/pact-foundation/pact-reference/issues/451#issuecomment-2338130587
136
+ # PACT_DESCRIPTION
137
+ # Only validate interactions whose descriptions match this filter (regex format)
138
+ # PACT_PROVIDER_STATE
139
+ # Only validate interactions whose provider states match this filter (regex format)
140
+ # PACT_PROVIDER_NO_STATE
141
+ # Only validate interactions that have no defined provider state (true or false)
142
+ PactFfi::Verifier.set_filter_info(
143
+ handle,
144
+ ENV["PACT_DESCRIPTION"] || nil,
145
+ ENV["PACT_PROVIDER_STATE"] || nil,
146
+ bool_to_int(ENV["PACT_PROVIDER_NO_STATE"] || false)
147
+ )
148
+
149
+ Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
150
+
151
+ logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}")
152
+
153
+ handle
154
+ end
155
+
156
+ def set_provider_info(pact_handle)
157
+ # pub extern "C" fn pactffi_verifier_set_provider_info(
158
+ # handle: *mut VerifierHandle,
159
+ # name: *const c_char,
160
+ # scheme: *const c_char,
161
+ # host: *const c_char,
162
+ # port: c_ushort,
163
+ # path: *const c_char,
164
+ # ) {
165
+ PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", 0, "")
166
+ end
167
+
168
+ def add_provider_transport(pact_handle)
169
+ raise Pact::V2::ImplementationRequired, "Implement #add_provider_transport in a subclass"
170
+ end
171
+
172
+ def start_servers!
173
+ logger.info("[verifier] starting services")
174
+
175
+ @servers_started = true
176
+ @pact_config.start_servers
177
+ end
178
+
179
+ def stop_servers
180
+ return unless @servers_started
181
+
182
+ logger.info("[verifier] stopping services")
183
+
184
+ @pact_config.stop_servers
185
+ end
186
+
187
+ def configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
188
+ logger.info("[verifier] configuring verification source")
189
+ if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank?
190
+ # todo support non rail apps
191
+ path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join("pacts").to_s : "pacts")
192
+ logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source")
193
+ return PactFfi::Verifier.add_directory_source(handle, path)
194
+ end
195
+
196
+ if @pact_config.pact_uri.present?
197
+ if @pact_config.pact_uri.start_with?("http")
198
+ logger.info("[verifier] using pact uri #{@pact_config.pact_uri} as a verification source")
199
+ PactFfi::Verifier.url_source(handle, @pact_config.pact_uri, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token)
200
+ else
201
+ logger.info("[verifier] using pact file #{@pact_config.pact_uri} as a verification source")
202
+ PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri)
203
+ end
204
+ else
205
+ logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source")
206
+ consumer_selectors = [] if consumer_selectors.nil?
207
+ filters = consumer_selectors.map do |selector|
208
+ FFI::MemoryPointer.from_string(JSON.dump(selector).to_s)
209
+ end
210
+ filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1)
211
+ filters_ptr.write_array_of_pointer(filters)
212
+ PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size)
213
+ end
214
+ end
215
+
216
+ def consumer_selectors
217
+ (!@pact_config.consumer_version_selectors.empty? && @pact_config.consumer_version_selectors) || @consumer_selectors if @pact_config.consumer_version_selectors
218
+ end
219
+
220
+ def build_consumer_selectors(verify_only, consumer_name, consumer_branch)
221
+ # if verify_only and consumer_name are defined - select only needed consumer
222
+ if verify_only.present?
223
+ # select proper consumer branch if defined
224
+ if consumer_name.present?
225
+ return [] unless verify_only.include?(consumer_name)
226
+ return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_branch.present?
227
+ return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)]
228
+ end
229
+ # or default selectors
230
+ return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge("consumer" => name) }
231
+ end
232
+
233
+ # select provided consumer_name
234
+ return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_name.present? && consumer_branch.present?
235
+ return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] if consumer_name.present?
236
+
237
+ [DEFAULT_CONSUMER_SELECTORS]
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+
6
+ module Pact
7
+ module V2
8
+ module Provider
9
+ class GrpcVerifier < BaseVerifier
10
+ PROVIDER_TRANSPORT_TYPE = "grpc"
11
+
12
+ def initialize(pact_config, mixed_config = nil)
13
+ super
14
+
15
+ raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Grpc" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Grpc)
16
+ @grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", services: @pact_config.grpc_services)
17
+ end
18
+
19
+ private
20
+
21
+ def add_provider_transport(pact_handle)
22
+ PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, "", "")
23
+ end
24
+
25
+
26
+ def start_servers!
27
+ super
28
+ @grpc_server.start
29
+ end
30
+
31
+ def stop_servers
32
+ super
33
+ @grpc_server.stop
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end