Skip to content

Commit

Permalink
feat: add generate_contents methods
Browse files Browse the repository at this point in the history
The various `get_contents` FFI method return the raw (unprocessed)
contents of the request/response, including all generators and matching
rules. The `generate_contents` alternatives are used to process these
generators so that the actual messages as received by the consumer can
be retrieved.

Signed-off-by: JP-Ellis <josh@jpellis.me>
  • Loading branch information
JP-Ellis committed Jun 19, 2024
1 parent 3900dfa commit a18b774
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 7 deletions.
69 changes: 67 additions & 2 deletions rust/pact_ffi/src/models/async_message.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
//! V4 ASynchronous messages
use std::collections::HashMap;

use anyhow::anyhow;
use bytes::Bytes;
use futures::executor::block_on;
use libc::{c_char, c_int, c_uchar, c_uint, EXIT_FAILURE, EXIT_SUCCESS, size_t};
use pact_matching::generators::apply_generators_to_async_message;
use pact_models::bodies::OptionalBody;
use pact_models::content_types::{ContentType, ContentTypeHint};
use pact_models::generators::GeneratorTestMode;
use pact_models::provider_states::ProviderState;
use pact_models::v4::async_message::AsynchronousMessage;
use pact_models::v4::message_parts::MessageContents;
Expand Down Expand Up @@ -59,6 +64,41 @@ ffi_fn! {
}
}

ffi_fn! {
/// Generate the message contents of an `AsynchronousMessage` as a
/// `MessageContents` pointer.
///
/// This function differs from [`pactffi_async_message_get_contents`] in
/// that it will process the message contents for any generators or matchers
/// that are present in the message in order to generate the actual message
/// contents as would be received by the consumer.
///
/// # Safety
///
/// The data pointed to by the pointer must be deleted with
/// [`pactffi_message_contents_delete`][crate::models::contents::pactffi_message_contents_delete]
///
/// # Error Handling
///
/// If the message is NULL, returns NULL.
fn pactffi_async_message_generate_contents(message: *const AsynchronousMessage) -> *const MessageContents {
let message = as_ref!(message);
let context = HashMap::new();
let plugin_data = Vec::new();
let interaction_data = HashMap::new();
let contents = block_on(apply_generators_to_async_message(
&message,
&GeneratorTestMode::Consumer,
&context,
&plugin_data,
&interaction_data,
));
ptr::raw_to(contents) as *const MessageContents
} {
std::ptr::null()
}
}

ffi_fn! {
/// Get the message contents of an `AsynchronousMessage` in string form.
///
Expand Down Expand Up @@ -321,12 +361,16 @@ mod tests {
use expectest::prelude::*;
use libc::c_char;

use crate::models::async_message::{
use pact_models::generators;
use pact_models::generators::Generator;

use super::{
pactffi_async_message_delete,
pactffi_async_message_generate_contents,
pactffi_async_message_get_contents_length,
pactffi_async_message_get_contents_str,
pactffi_async_message_new,
pactffi_async_message_set_contents_str
pactffi_async_message_set_contents_str,
};

#[test]
Expand All @@ -344,4 +388,25 @@ mod tests {
expect!(str.to_str().unwrap()).to(be_equal_to("This is a string"));
expect!(len).to(be_equal_to(16));
}

#[test]
fn test_generate_contents() {
let message = pactffi_async_message_new();
let message_contents = CString::new(r#"{ "id": 1 }"#).unwrap();
let content_type = CString::new("application/json").unwrap();
pactffi_async_message_set_contents_str(message, message_contents.as_ptr(), content_type.as_ptr());

unsafe { &mut *message }.contents.generators.add_generators(generators!{
"body" => {
"$.id" => Generator::RandomInt(1000, 1000)
}
});

let contents = pactffi_async_message_generate_contents(message);

assert_eq!(
r#"{"id":1000}"#,
unsafe { &*contents }.contents.value_as_string().unwrap()
);
}
}
16 changes: 16 additions & 0 deletions rust/pact_ffi/src/models/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ use crate::models::message::MessageMetadataIterator;
use crate::string::optional_str;
use crate::util::*;

ffi_fn! {
/// Delete the message contents instance.
///
/// # Safety
///
/// This should only be called on a message contents that require deletion.
/// The function creating the message contents should document whether it
/// requires deletion.
///
/// Deleting a message content which is associated with an interaction
/// will result in undefined behaviour.
fn pactffi_message_contents_delete(contents: *const MessageContents) {
ptr::drop_raw(contents as *mut MessageContents);
}
}

ffi_fn! {
/// Get the message contents in string form.
///
Expand Down
115 changes: 110 additions & 5 deletions rust/pact_ffi/src/models/sync_message.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
//! V4 Synchronous request/response messages
use std::collections::HashMap;

use anyhow::anyhow;
use bytes::Bytes;
use futures::executor::block_on;
use libc::{c_char, c_int, c_uchar, c_uint, EXIT_FAILURE, EXIT_SUCCESS, size_t};
use pact_matching::generators::apply_generators_to_sync_message;
use pact_models::bodies::OptionalBody;
use pact_models::content_types::{ContentType, ContentTypeHint};
use pact_models::generators::GeneratorTestMode;
use pact_models::provider_states::ProviderState;
use pact_models::v4::message_parts::MessageContents;
use pact_models::v4::sync_message::SynchronousMessage;
Expand Down Expand Up @@ -209,6 +214,41 @@ ffi_fn! {
}
}

ffi_fn! {
/// Generate the request contents of a `SynchronousMessage` as a
/// `MessageContents` pointer.
///
/// This function differs from [`pactffi_sync_message_get_request_contents`]
/// in that it will process the message contents for any generators or
/// matchers that are present in the message in order to generate the actual
/// message contents as would be received by the consumer.
///
/// # Safety
///
/// The data pointed to by the pointer must be deleted with
/// [`pactffi_message_contents_delete`][crate::models::contents::pactffi_message_contents_delete]
///
/// # Error Handling
///
/// If the message is NULL, returns NULL.
fn pactffi_sync_message_generate_request_contents(message: *const SynchronousMessage) -> *const MessageContents {
let message = as_ref!(message);
let context = HashMap::new();
let plugin_data = Vec::new();
let interaction_data = HashMap::new();
let (contents, _) = block_on(apply_generators_to_sync_message(
&message,
&GeneratorTestMode::Consumer,
&context,
&plugin_data,
&interaction_data,
));
ptr::raw_to(contents) as *const MessageContents
} {
std::ptr::null()
}
}

ffi_fn! {
/// Get the number of response messages in the `SynchronousMessage`.
///
Expand Down Expand Up @@ -437,6 +477,46 @@ ffi_fn! {
}
}

ffi_fn! {
/// Generate the response contents of a `SynchronousMessage` as a
/// `MessageContents` pointer.
///
/// This function differs from
/// [`pactffi_sync_message_get_response_contents`] in that it will process
/// the message contents for any generators or matchers that are present in
/// the message in order to generate the actual message contents as would be
/// received by the consumer.
///
/// # Safety
///
/// The data pointed to by the pointer must be deleted with
/// [`pactffi_message_contents_delete`][crate::models::contents::pactffi_message_contents_delete]
///
/// # Error Handling
///
/// If the message is NULL, returns NULL.
fn pactffi_sync_message_generate_response_contents(message: *const SynchronousMessage, index: size_t) -> *const MessageContents {
let message = as_ref!(message);
if index >= message.response.len() {
return Ok(std::ptr::null());
}

let context = HashMap::new();
let plugin_data = Vec::new();
let interaction_data = HashMap::new();
let (_, mut responses) = block_on(apply_generators_to_sync_message(
&message,
&GeneratorTestMode::Consumer,
&context,
&plugin_data,
&interaction_data,
));
ptr::raw_to(responses.swap_remove(index)) as *const MessageContents
} {
std::ptr::null()
}
}

ffi_fn! {
/// Get a copy of the description.
///
Expand Down Expand Up @@ -551,18 +631,22 @@ mod tests {
use expectest::prelude::*;
use libc::c_char;

use crate::models::sync_message::{
use pact_models::generators;
use pact_models::generators::Generator;

use super::{
pactffi_sync_message_delete,
pactffi_sync_message_get_request_contents_str,
pactffi_sync_message_generate_request_contents,
pactffi_sync_message_get_request_contents_length,
pactffi_sync_message_get_response_contents_str,
pactffi_sync_message_get_request_contents_str,
pactffi_sync_message_get_response_contents_length,
pactffi_sync_message_get_response_contents_str,
pactffi_sync_message_new,
pactffi_sync_message_set_request_contents_str,
pactffi_sync_message_set_response_contents_str
pactffi_sync_message_set_response_contents_str,
};

#[test]
#[test]
fn get_and_set_message_contents() {
let message = pactffi_sync_message_new();
let message_contents = CString::new("This is a string").unwrap();
Expand Down Expand Up @@ -596,4 +680,25 @@ mod tests {
expect!(response_str2.to_str().unwrap()).to(be_equal_to("This is another string"));
expect!(response_len2).to(be_equal_to(22));
}

#[test]
fn test_generate_contents() {
let message = pactffi_sync_message_new();
let message_contents = CString::new(r#"{ "id": 1 }"#).unwrap();
let content_type = CString::new("application/json").unwrap();
pactffi_sync_message_set_request_contents_str(message, message_contents.as_ptr(), content_type.as_ptr());

unsafe { &mut *message }.request.generators.add_generators(generators!{
"body" => {
"$.id" => Generator::RandomInt(1000, 1000)
}
});

let contents = pactffi_sync_message_generate_request_contents(message);

assert_eq!(
r#"{"id":1000}"#,
unsafe { &*contents }.contents.value_as_string().unwrap()
);
}
}

0 comments on commit a18b774

Please sign in to comment.