Skip to content

Commit

Permalink
feat: add FFI function to parse a matching definition expression
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Nov 10, 2022
1 parent e8e75db commit 768a132
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 0 deletions.
210 changes: 210 additions & 0 deletions rust/pact_ffi/src/models/expressions.rs
@@ -0,0 +1,210 @@
//! Functions for dealing with matching rule expressions

use anyhow::Context;
use either::Either;
use libc::c_char;
use pact_models::matchingrules::expressions::{
is_matcher_def,
MatchingRuleDefinition,
parse_matcher_def,
ValueType
};
use tracing::{debug, error};

use crate::{as_ref, ffi_fn, safe_str};
use crate::util::{ptr, string};

/// Result of parsing a matching rule definition
#[derive(Debug, Clone)]
pub struct MatchingRuleDefinitionResult {
result: Either<String, MatchingRuleDefinition>
}

ffi_fn! {
/// Parse a matcher definition string into a MatchingRuleDefinition containing the example value,
/// and matching rules and any generator.
///
/// The following are examples of matching rule definitions:
/// * `matching(type,'Name')` - type matcher with string value 'Name'
/// * `matching(number,100)` - number matcher
/// * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with format string
///
/// See [Matching Rule definition expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html).
///
/// The returned value needs to be freed up with the `pactffi_matcher_definition_delete` function.
///
/// # Errors
/// If the expression is invalid, the MatchingRuleDefinition error will be set. You can check for
/// this value with the `pactffi_matcher_definition_error` function.
///
/// # Safety
///
/// This function is safe if the expression is a valid NULL terminated string pointer.
fn pactffi_parse_matcher_definition(expression: *const c_char) -> *const MatchingRuleDefinitionResult {
let expression = safe_str!(expression);
let result = if is_matcher_def(expression) {
match parse_matcher_def(expression) {
Ok(definition) => {
debug!("Parsed matcher definition '{}' to '{:?}'", expression, definition);
MatchingRuleDefinitionResult {
result: Either::Right(definition)
}
}
Err(err) => {
error!("Failed to parse matcher definition '{}': {}", expression, err);
MatchingRuleDefinitionResult {
result: Either::Left(err.to_string())
}
}
}
} else if expression.is_empty() {
MatchingRuleDefinitionResult {
result: Either::Left("Expected a matching rule definition, but got an empty string".to_string())
}
} else {
MatchingRuleDefinitionResult {
result: Either::Right(MatchingRuleDefinition {
value: expression.to_string(),
value_type: ValueType::String,
rules: vec![],
generator: None
})
}
};

ptr::raw_to(result) as *const MatchingRuleDefinitionResult
} {
ptr::null_to::<MatchingRuleDefinitionResult>()
}
}

ffi_fn! {
/// Returns any error message from parsing a matching definition expression. If there is no error,
/// it will return a NULL pointer, otherwise returns the error message as a NULL-terminated string.
/// The returned string must be freed using the `pactffi_string_delete` function once done with it.
fn pactffi_matcher_definition_error(definition: *const MatchingRuleDefinitionResult) -> *const c_char {
let definition = as_ref!(definition);
if let Either::Left(error) = &definition.result {
string::to_c(&error)? as *const c_char
} else {
ptr::null_to::<c_char>()
}
} {
ptr::null_to::<c_char>()
}
}

ffi_fn! {
/// Returns the value from parsing a matching definition expression. If there was an error,
/// it will return a NULL pointer, otherwise returns the value as a NULL-terminated string.
/// The returned string must be freed using the `pactffi_string_delete` function once done with it.
fn pactffi_matcher_definition_value(definition: *const MatchingRuleDefinitionResult) -> *const c_char {
let definition = as_ref!(definition);
if let Either::Right(definition) = &definition.result {
string::to_c(&definition.value)? as *const c_char
} else {
ptr::null_to::<c_char>()
}
} {
ptr::null_to::<c_char>()
}
}

ffi_fn! {
/// Frees the memory used by the result of parsing the matching definition expression
fn pactffi_matcher_definition_delete(definition: *const MatchingRuleDefinitionResult) {
ptr::drop_raw(definition as *mut MatchingRuleDefinitionResult);
}
}

#[cfg(test)]
mod tests {
use std::ffi::CString;

use expectest::prelude::*;
use libc::c_char;

use crate::models::expressions::{
MatchingRuleDefinitionResult,
pactffi_matcher_definition_error,
pactffi_matcher_definition_value,
pactffi_parse_matcher_definition
};
use crate::util::ptr;

#[test]
fn parse_expression_with_null() {
let result = pactffi_parse_matcher_definition(ptr::null_to());
expect!(result.is_null()).to(be_true());
}

#[test]
fn parse_expression_with_empty_string() {
let empty = CString::new("").unwrap();
let result = pactffi_parse_matcher_definition(empty.as_ptr());
expect!(result.is_null()).to(be_false());

let error = pactffi_matcher_definition_error(result);
let string = unsafe { CString::from_raw(error as *mut c_char) };
expect!(string.to_string_lossy()).to(be_equal_to("Expected a matching rule definition, but got an empty string"));

let definition = unsafe { Box::from_raw(result as *mut MatchingRuleDefinitionResult) };
expect!(definition.result.left()).to(be_some().value("Expected a matching rule definition, but got an empty string"));
}

#[test]
fn parse_expression_with_invalid_expression() {
let value = CString::new("matching(type,").unwrap();
let result = pactffi_parse_matcher_definition(value.as_ptr());
expect!(result.is_null()).to(be_false());

let error = pactffi_matcher_definition_error(result);
let string = unsafe { CString::from_raw(error as *mut c_char) };
expect!(string.to_string_lossy()).to(be_equal_to("expected a primitive value"));

let value = pactffi_matcher_definition_value(result);
expect!(value.is_null()).to(be_true());

let definition = unsafe { Box::from_raw(result as *mut MatchingRuleDefinitionResult) };
expect!(definition.result.left()).to(be_some().value("expected a primitive value"));
}

#[test]
fn parse_expression_with_valid_expression() {
let value = CString::new("matching(type,'Name')").unwrap();
let result = pactffi_parse_matcher_definition(value.as_ptr());
expect!(result.is_null()).to(be_false());

let error = pactffi_matcher_definition_error(result);
expect!(error.is_null()).to(be_true());

let value = pactffi_matcher_definition_value(result);
expect!(value.is_null()).to(be_false());
let string = unsafe { CString::from_raw(value as *mut c_char) };
expect!(string.to_string_lossy()).to(be_equal_to("Name"));

let definition = unsafe { Box::from_raw(result as *mut MatchingRuleDefinitionResult) };
expect!(definition.result.as_ref().left()).to(be_none());
expect!(definition.result.as_ref().right()).to(be_some());
}

#[test]
fn parse_expression_with_normal_string() {
let value = CString::new("I am not an expression").unwrap();
let result = pactffi_parse_matcher_definition(value.as_ptr());
expect!(result.is_null()).to(be_false());

let error = pactffi_matcher_definition_error(result);
expect!(error.is_null()).to(be_true());

let value = pactffi_matcher_definition_value(result);
expect!(value.is_null()).to(be_false());
let string = unsafe { CString::from_raw(value as *mut c_char) };
expect!(string.to_string_lossy()).to(be_equal_to("I am not an expression"));

let definition = unsafe { Box::from_raw(result as *mut MatchingRuleDefinitionResult) };
expect!(definition.result.as_ref().left()).to(be_none());
expect!(definition.result.as_ref().right()).to(be_some());
expect!(definition.result.as_ref().right().unwrap().rules.is_empty()).to(be_true());
}
}
1 change: 1 addition & 0 deletions rust/pact_ffi/src/models/mod.rs
Expand Up @@ -9,3 +9,4 @@ pub mod provider_state;
pub mod iterators;
pub mod sync_message;
pub mod http_interaction;
pub mod expressions;

0 comments on commit 768a132

Please sign in to comment.