Skip to content

Instantly share code, notes, and snippets.

@mcon-gr
Last active May 4, 2023 01:02
Show Gist options
  • Save mcon-gr/348232a3d2e64df544858b5491bb9d30 to your computer and use it in GitHub Desktop.
Save mcon-gr/348232a3d2e64df544858b5491bb9d30 to your computer and use it in GitHub Desktop.
Pact example suggestion with Protobuf
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}

Requirements for adding Protobuf support to the Pact mock_service

https://github.com/pact-foundation/pact-mock_service

This only covers a suggestion of how to add Protobuf support for just the response functionality of the mock_service.

The sugegsted workflow for creating consumer tests with protobuf is assumed to be as follows:

  • Write the Contract.proto file which describes the message which will be the payload for an HTTP response from a provider.
  • Generate the Google.Protobuf code which handles the messages in the .proto.
  • Program the interaction into the mock_server by posting interaction-post-payload.json to it.
  • The file_descriptor_set_b64 is a representation which contains the details of how to encode/decode the Person message - it's possible to construct these from generated code. Given .proto definitions often don't live in the repo of the application, we need an approach that doesn't rely on the .proto contents (there are other reasons too why the file descriptor set is the best thing here).
  • Generate the ruby code required to encode/decode using protoc, put this in the temp directory.
  • When the consumer test code attempts to access /hello, the mock_service knows that the response programmed is protobuf: it then loads all the generated proto code, and exercises the right methods to encode the response in protobuf.

Migration points

  • The intention is to make content_type optional, assuming JSON if it's not present, and protobuf_context would also be optional in the response, which makes this new feature possible without breaking existing clients.
  • I think if this were to be a supported thing, then a mention of JSON-izeable formats, protobuf specifically, would be a good idea.

Remaining points

The following aspects still need to be clarified:

  • The request decoding in the mock_service.
  • Both encoding and decoding in pact-provider-verifier. I haven't attempted to lay out my strategy for these, as I think it will be fairly clear that what I'm suggesting here is a thin wrapper around all of the existing functionality. Given all I'm tyring to do with protobuf is encode/decode when data is going on / coming off the wire, and the data remains JSON otherwise - we're not talking about a change that's super invasive.

Given that expected requests/responses are transformed straight into JSON, there are no protobuf-specific concerns WRT matching functionality.

Note

None of the code included in this gist is pretending to be production ready: it's just an illustration.

I have a working (very early) prototype working based on this gist: if you approve of this approach, I'm happy to flesh it out and create a PR and likewise do the same for the pact-provider-verifier.

{
"description": "hello world",
"request": {
"method": "Get",
"path": "/hello"
},
"response": {
"status": 200,
"content_type": "protobuf",
"protobuf_context": {
"proto_file": "contract",
"proto_msg_name": "Person",
"file_descriptor_set_b64": "CmYKDmNvbnRyYWN0LnByb3RvEghjb250cmFjdCJCCgZQZXJzb24SEgoEbmFtZRgBIAEoCVIEbmFtZRIOCgJpZBgCIAEoBVICaWQSFAoFZW1haWwYAyABKAlSBWVtYWlsYgZwcm90bzM=",
},
"body": {
"name": "foobar",
"id": 123,
"email": "foo@bar.com"
}
}
}
class HandleMatchedInteraction
def self.response_from response
if (response[:content_type] == "protobuf")
# Placeholder method for taking the response body JSON and transforming it (using the protobuf generated code)
# into protobuf to be sent over the wire.
body = Pact::MockService::ProtobufEncode.call(response[:protobuf_context], response.body.to_json)
else
# Render the JSON body as normal
body = render_body(Pact::Reification.from_term(response.body))
end
[response.status, (Pact::Reification.from_term(response.headers) || {}).to_hash, [body]]
end
def self.render_body body
return '' unless body
body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
end
end
class Response < Hash
...
# Add the content_type and protobuf_context keys to the response so that we persist them.
ALLOWED_KEYS = [:status, :headers, :body, :content_type, :protobuf_context,
'status', 'headers', 'body', 'content_type', 'protobuf_context'].freeze
private_constant :ALLOWED_KEYS
...
end
def really_add_expected_interaction interaction
if (interaction.response[:content_type] == "protobuf")
# Placeholder for generating the Ruby code required for encoding responses from the mock_service
# The code required to read the protobuf in question is generated and placed in Pact.configuration.tmp_dir
# for use later, when the mock_service is replaying interactions.
ProtobufCodegen.call(interaction.response[:protobuf_context].content_descriptor, interaction.response[:protobuf_context].proto_file)
end
expected_interactions << interaction
logger.info "Registered expected interaction #{interaction.request.method_and_path}"
logger.debug JSON.pretty_generate InteractionDecorator.new(interaction)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment