#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(missing_copy_implementations)]
use std::ffi::CStr;
use std::panic::RefUnwindSafe;
use std::str::FromStr;
use lazy_static::lazy_static;
use libc::c_char;
use tracing::{debug, error, info, trace, warn};
use tracing_core::{Level, LevelFilter};
use tracing_log::AsLog;
use tracing_subscriber::FmtSubscriber;
use models::message::Message;
use pact_matching as pm;
pub use pact_matching::Mismatch;
use pact_models::interaction::Interaction;
use pact_models::pact::Pact;
use pact_models::v4::pact::V4Pact;
use crate::util::*;
pub mod error;
pub mod log;
pub mod models;
pub(crate) mod util;
pub mod mock_server;
pub mod verifier;
pub mod plugins;
pub mod matching;
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
lazy_static! {
static ref RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Could not start a Tokio runtime for running async tasks");
}
#[no_mangle]
pub extern "C" fn pactffi_version() -> *const c_char {
VERSION.as_ptr() as *const c_char
}
#[no_mangle]
pub unsafe extern fn pactffi_init(log_env_var: *const c_char) {
let log_env_var = if !log_env_var.is_null() {
let c_str = CStr::from_ptr(log_env_var);
match c_str.to_str() {
Ok(str) => str,
Err(err) => {
warn!("Failed to parse the environment variable name as a UTF-8 string: {}", err);
"LOG_LEVEL"
}
}
} else {
"LOG_LEVEL"
};
let subscriber = FmtSubscriber::builder()
.with_env_filter(log_env_var)
.with_thread_names(true)
.with_ansi(false) .finish();
if let Err(err) = tracing::subscriber::set_global_default(subscriber) {
eprintln!("Failed to initialise global tracing subscriber - {err}");
};
}
#[no_mangle]
pub unsafe extern "C" fn pactffi_init_with_log_level(level: *const c_char) {
let log_level = log_level_filter_from_c_char(level);
let subscriber = FmtSubscriber::builder()
.with_max_level(log_level)
.with_thread_names(true)
.with_ansi(false) .finish();
if let Err(err) = tracing::subscriber::set_global_default(subscriber) {
eprintln!("Failed to initialise global tracing subscriber - {err}");
};
}
#[no_mangle]
#[cfg(windows)]
pub extern "C" fn pactffi_enable_ansi_support() {
if let Err(err) = ansi_term::enable_ansi_support() {
warn!("Could not enable ANSI console support - {err}");
}
}
#[no_mangle]
#[cfg(not(windows))]
pub extern "C" fn pactffi_enable_ansi_support() { }
#[no_mangle]
pub unsafe extern "C" fn pactffi_log_message(source: *const c_char, log_level: *const c_char, message: *const c_char) {
let target = convert_cstr("target", source).unwrap_or("client");
if !message.is_null() {
if let Some(message) = convert_cstr("message", message) {
match log_level_from_c_char(log_level).as_log() {
::log::Level::Error => error!(source = target, "{}", message),
::log::Level::Warn => warn!(source = target, "{}", message),
::log::Level::Info => info!(source = target, "{}", message),
::log::Level::Debug => debug!(source = target, "{}", message),
::log::Level::Trace => trace!(source = target, "{}", message)
}
}
}
}
unsafe fn log_level_from_c_char(log_level: *const c_char) -> Level {
if !log_level.is_null() {
let level = convert_cstr("log_level", log_level).unwrap_or("INFO");
Level::from_str(level).unwrap_or(tracing::Level::INFO)
} else {
Level::INFO
}
}
unsafe fn log_level_filter_from_c_char(log_level: *const c_char) -> LevelFilter {
if !log_level.is_null() {
let level = convert_cstr("log_level", log_level).unwrap_or("INFO");
match level.to_lowercase().as_str() {
"none" => LevelFilter::OFF,
_ => LevelFilter::from_str(level).unwrap_or(LevelFilter::INFO)
}
} else {
LevelFilter::INFO
}
}
fn convert_cstr(name: &str, value: *const c_char) -> Option<&str> {
unsafe {
if value.is_null() {
warn!("{} is NULL!", name);
None
} else {
let c_str = CStr::from_ptr(value);
match c_str.to_str() {
Ok(str) => Some(str),
Err(err) => {
warn!("Failed to parse {} name as a UTF-8 string: {}", name, err);
None
}
}
}
}
}
ffi_fn! {
fn pactffi_match_message(msg_1: *const Message, msg_2: *const Message) -> *const Mismatches {
let msg_1: Box<dyn Interaction + Send + Sync + RefUnwindSafe> = unsafe { Box::from_raw(msg_1 as *mut Message) };
let msg_2: Box<dyn Interaction + Send + Sync + RefUnwindSafe> = unsafe { Box::from_raw(msg_2 as *mut Message) };
let mismatches = RUNTIME.block_on(async move {
Mismatches(pm::match_message(&msg_1, &msg_2, &V4Pact::default().boxed()).await)
});
ptr::raw_to(mismatches) as *const Mismatches
} {
std::ptr::null() as *const Mismatches
}
}
ffi_fn! {
fn pactffi_mismatches_get_iter(mismatches: *const Mismatches) -> *mut MismatchesIterator {
let mismatches = as_ref!(mismatches);
let iter = MismatchesIterator { current: 0, mismatches };
ptr::raw_to(iter)
} {
std::ptr::null_mut()
}
}
ffi_fn! {
fn pactffi_mismatches_delete(mismatches: *const Mismatches) {
ptr::drop_raw(mismatches as *mut Mismatches);
}
}
ffi_fn! {
fn pactffi_mismatches_iter_next(iter: *mut MismatchesIterator) -> *const Mismatch {
let iter = as_mut!(iter);
let mismatches = as_ref!(iter.mismatches);
let index = iter.next();
match mismatches.0.get(index) {
Some(mismatch) => mismatch as *const Mismatch,
None => {
trace!("iter past the end of mismatches");
std::ptr::null()
}
}
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_mismatches_iter_delete(iter: *mut MismatchesIterator) {
ptr::drop_raw(iter);
}
}
ffi_fn! {
fn pactffi_mismatch_to_json(mismatch: *const Mismatch) -> *const c_char {
let mismatch = as_ref!(mismatch);
let json = mismatch.to_json().to_string();
string::to_c(&json)? as *const c_char
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_mismatch_type(mismatch: *const Mismatch) -> *const c_char {
let mismatch = as_ref!(mismatch);
let t = mismatch.mismatch_type();
string::to_c(t)? as *const c_char
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_mismatch_summary(mismatch: *const Mismatch) -> *const c_char {
let mismatch = as_ref!(mismatch);
let summary = mismatch.summary();
string::to_c(&summary)? as *const c_char
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_mismatch_description(mismatch: *const Mismatch) -> *const c_char {
let mismatch = as_ref!(mismatch);
let description = mismatch.description();
string::to_c(&description)? as *const c_char
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_mismatch_ansi_description(mismatch: *const Mismatch) -> *const c_char {
let mismatch = as_ref!(mismatch);
let ansi_description = mismatch.ansi_description();
string::to_c(&ansi_description)? as *const c_char
} {
std::ptr::null()
}
}
#[allow(missing_copy_implementations)]
#[allow(missing_debug_implementations)]
pub struct Mismatches(Vec<Mismatch>);
#[allow(missing_copy_implementations)]
#[allow(missing_debug_implementations)]
pub struct MismatchesIterator {
current: usize,
mismatches: *const Mismatches,
}
impl MismatchesIterator {
fn next(&mut self) -> usize {
let idx = self.current;
self.current += 1;
idx
}
}
#[cfg(test)]
mod tests {
use std::ffi::CString;
use expectest::prelude::*;
use rstest::rstest;
use super::*;
use tracing_core::LevelFilter;
#[rstest]
#[case("trace", LevelFilter::TRACE)]
#[case("TRACE", LevelFilter::TRACE)]
#[case("debug", LevelFilter::DEBUG)]
#[case("DEBUG", LevelFilter::DEBUG)]
#[case("info", LevelFilter::INFO)]
#[case("INFO", LevelFilter::INFO)]
#[case("warn", LevelFilter::WARN)]
#[case("WARN", LevelFilter::WARN)]
#[case("error", LevelFilter::ERROR)]
#[case("ERROR", LevelFilter::ERROR)]
#[case("off", LevelFilter::OFF)]
#[case("OFF", LevelFilter::OFF)]
#[case("none", LevelFilter::OFF)]
#[case("NONE", LevelFilter::OFF)]
#[case("invalid", LevelFilter::INFO)]
fn log_level_filter_from_c_char_test(#[case] text: String, #[case] level: LevelFilter) {
let value = CString::new(text).unwrap();
let result = unsafe { log_level_filter_from_c_char(value.as_ptr()) };
expect!(result).to(be_equal_to(level));
}
#[rstest]
#[case("trace", Level::TRACE)]
#[case("TRACE", Level::TRACE)]
#[case("debug", Level::DEBUG)]
#[case("DEBUG", Level::DEBUG)]
#[case("info", Level::INFO)]
#[case("INFO", Level::INFO)]
#[case("warn", Level::WARN)]
#[case("WARN", Level::WARN)]
#[case("error", Level::ERROR)]
#[case("ERROR", Level::ERROR)]
#[case("off", Level::INFO)]
#[case("OFF", Level::INFO)]
#[case("none", Level::INFO)]
#[case("NONE", Level::INFO)]
#[case("invalid", Level::INFO)]
fn log_level_from_c_char_test(#[case] text: String, #[case] level: Level) {
let value = CString::new(text).unwrap();
let result = unsafe { log_level_from_c_char(value.as_ptr()) };
expect!(result).to(be_equal_to(level));
}
}