Protobuf Plugin prototype
Source Code​
https://github.com/pact-foundation/pact-plugins/tree/main/plugins
This is an example plugin supporting creating and matching Protobuf messages (proto3 version).
Building the plugin​
The plugin is built with Gradle. Just run ./gradlew installDist
. This will create the plugin archive file in the
build/distributions
directory. There will be a Zip and Tar bundle.
Installing the plugin​
The plugin bundle and manifest file pact-plugin.json need to be unpacked/copied into the $HOME/.pact/plugins/protobuf-0.0.0
directory.
You can download the bundle and manifest from the release for the plugin.
There is also a Gradle task installLocal
that will build the plugin and unpack it into the installation directory.
Example Projects​
There are three example projects in examples/protobuf that use this plugin:
- protobuf-consumer - consumer written in Java
- protobuf-consumer-rust - consumer written in Rust
- protobuf-provider - provider written in Go
Protobuf matching definitions​
The plugin matches the Protobuf messages using matching rule definitions. It supports normal, repeated and map fields.
Each message needs to be configured by a map of field names to matching definitions. For instance, given the following message:
message InitPluginRequest {
string implementation = 1;
string version = 2;
}
the consumer test can be configured with:
builder
.usingPlugin("protobuf") // Tell pact to load the plugin for the test
.expectsToReceive("init plugin message", "core/interaction/message") // will use a message interaction
.with(Map.of(
"message.contents", Map.of(
"pact:proto", filePath("../../../proto/plugin.proto"), // Need to provide the proto file
"pact:message-type", "InitPluginRequest", // The message in the proto file we will be testing with
"pact:content-type", "application/protobuf", // Required content type for protobuf test
"implementation", "notEmpty('pact-jvm-driver')", // Require the `implementation` to not be empty (must be present and not the empty string)
"version", "matching(semver, '0.0.0')" // Require the `version` field to match the semver spec
)
))
.toPact()
Message fields​
Fields that are messages can be matched by specifying a map for the attribute.
For example, with
message Body {
string contentType = 1;
google.protobuf.BytesValue content = 2;
enum ContentTypeHint {
DEFAULT = 0;
TEXT = 1;
BINARY = 2;
}
ContentTypeHint contentTypeHint = 3;
}
message InteractionResponse {
Body contents = 1;
}
the consumer test can be configured with:
builder
.usingPlugin("protobuf")
.expectsToReceive("Configure Interaction Response", "core/interaction/message")
.with(Map.of(
"message.contents", Map.of(
"pact:proto", filePath("../../../proto/plugin.proto"),
"pact:message-type", "InteractionResponse",
"pact:content-type", "application/protobuf",
"contents", Map.of( // contents is a message, so use a map to confugure the matching
"contentType", "notEmpty('application/json')", // contents.contentType must not be empty
"content", "matching(contentType, 'application/json', '{}')", // contents.content must contain JSON data
"contentTypeHint", "matching(equalTo, 'TEXT')" // contents.contentTypeHint must be equal to TEXT (enum value)
)
)
))
.toPact();
Map and repeated fields​
Map and repeated fields can be specified using a similar mechanism, but need a pact:match
entry that configures
how each item in the collection can be matched.
For example, given the following messages:
message MatchingRule {
string type = 1;
google.protobuf.Struct values = 2;
}
message MatchingRules {
repeated MatchingRule rule = 1;
}
message InteractionResponse {
map<string, MatchingRules> rules = 2;
}
you can configure the matching with
"rules", Map.of(
// Match each key in the map using a regex, and each item must match by type
// (the example will come from the map, so we can use null here)
"pact:match", "eachKey(matching(regex, '\\$(\\.\\w+)+', '$.test.one')), eachValue(matching(type, null))",
// This is the example map entry to use for matching
"$.test.one", Map.of(
"rule", Map.of(
// rule is a repeated field, so we define an "eachValue" matcher to match the item defined by "items"
"pact:match", "eachValue(matching($'items'))",
// the example to match each item in the "rule" collection
"items", Map.of(
"type", "notEmpty('regex')" // each item in the "rule" collection must have a "type" field that is not empty
)
)
)
Verifying the provider​
Verifying the provider just works as a normal message pact verification. In the provider example, there is a Go HTTP server that returns the Protobuf message based on the interaction description. Pointing the pact_verifier_cli at it to verify the pacts from the consumer tests works as normal. It needs to be version 0.9.0+ to support plugins.