From a18b7746cfc502feb45f20385f022283501ecf0f Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 17 Jun 2024 14:35:10 +1000 Subject: [PATCH] feat: add `generate_contents` methods 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 --- rust/pact_ffi/src/models/async_message.rs | 69 ++++++++++++- rust/pact_ffi/src/models/contents.rs | 16 +++ rust/pact_ffi/src/models/sync_message.rs | 115 +++++++++++++++++++++- 3 files changed, 193 insertions(+), 7 deletions(-) diff --git a/rust/pact_ffi/src/models/async_message.rs b/rust/pact_ffi/src/models/async_message.rs index 3fc3ef605..6c9bf4daa 100644 --- a/rust/pact_ffi/src/models/async_message.rs +++ b/rust/pact_ffi/src/models/async_message.rs @@ -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; @@ -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. /// @@ -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] @@ -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() + ); + } } diff --git a/rust/pact_ffi/src/models/contents.rs b/rust/pact_ffi/src/models/contents.rs index 15d6c0d79..7868d4371 100644 --- a/rust/pact_ffi/src/models/contents.rs +++ b/rust/pact_ffi/src/models/contents.rs @@ -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. /// diff --git a/rust/pact_ffi/src/models/sync_message.rs b/rust/pact_ffi/src/models/sync_message.rs index 911cc4280..6cf3fc221 100644 --- a/rust/pact_ffi/src/models/sync_message.rs +++ b/rust/pact_ffi/src/models/sync_message.rs @@ -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; @@ -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`. /// @@ -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. /// @@ -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(); @@ -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() + ); + } }