Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add FFI function to parse a matching definition expression
- Loading branch information
1 parent
e8e75db
commit 768a132
Showing
2 changed files
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ pub mod provider_state; | |
pub mod iterators; | ||
pub mod sync_message; | ||
pub mod http_interaction; | ||
pub mod expressions; |