Skip to main content

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.