use std::collections::HashMap;
use anyhow::anyhow;
use itertools::Itertools;
use libc::{c_char, c_ushort};
use maplit::hashmap;
use pact_models::generators::{
GeneratorCategory as CoreGeneratorCategory,
GenerateValue,
Generator,
NoopVariantMatcher,
VariantMatcher
};
use pact_models::path_exp::DocPath;
use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
use pact_models::v4::message_parts::MessageContents;
use serde_json::Value;
use tracing::{error, warn, trace};
use crate::{as_mut, as_ref, ffi_fn};
use crate::util::{ptr, string};
use crate::util::ptr::{drop_raw, raw_to};
ffi_fn! {
fn pactffi_generator_to_json(generator: *const Generator) -> *const c_char {
let generator = as_ref!(generator);
let json = generator.to_json().unwrap_or_default().to_string();
string::to_c(&json)? as *const c_char
} {
std::ptr::null()
}
}
ffi_fn! {
fn pactffi_generator_generate_string(
generator: *const Generator,
context_json: *const c_char
) -> *const c_char {
let generator = as_ref!(generator);
let context = string::optional_str(context_json);
let context_entries = context_map(context)?;
let map = context_entries.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
match generator.generate_value(&"".to_string(), &map, &NoopVariantMatcher.boxed()) {
Ok(value) => string::to_c(value.as_str())? as *const c_char,
Err(err) => {
error!("Failed to generate value - {}", err);
std::ptr::null()
}
}
} {
std::ptr::null()
}
}
fn context_map<'a>(context: Option<String>) -> anyhow::Result<serde_json::Map<String, Value>> {
if let Some(context) = context {
match serde_json::from_str::<Value>(context.as_str()) {
Ok(json) => match json {
Value::Object(entries) => Ok(entries.clone()),
_ => {
warn!("'{}' is not a JSON object, ignoring it", json);
Ok(serde_json::Map::default())
}
},
Err(err) => {
error!("Failed to parse the context value as JSON - {}", err);
Err(anyhow!("Failed to parse the context value as JSON - {}", err))
}
}
} else {
Ok(serde_json::Map::default())
}
}
ffi_fn! {
fn pactffi_generator_generate_integer(
generator: *const Generator,
context_json: *const c_char
) -> c_ushort {
let generator = as_ref!(generator);
let context = string::optional_str(context_json);
let context_entries = context_map(context)?;
let map = context_entries.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
match generator.generate_value(&0, &map, &NoopVariantMatcher.boxed()) {
Ok(value) => value,
Err(err) => {
error!("Failed to generate value - {}", err);
0
}
}
} {
0
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum GeneratorCategory {
METHOD,
PATH,
HEADER,
QUERY,
BODY,
STATUS,
METADATA
}
impl From<CoreGeneratorCategory> for GeneratorCategory {
#[inline]
fn from(category: CoreGeneratorCategory) -> GeneratorCategory {
match category {
CoreGeneratorCategory::METHOD => GeneratorCategory::METHOD,
CoreGeneratorCategory::PATH => GeneratorCategory::PATH,
CoreGeneratorCategory::HEADER => GeneratorCategory::HEADER,
CoreGeneratorCategory::QUERY => GeneratorCategory::QUERY,
CoreGeneratorCategory::BODY => GeneratorCategory::BODY,
CoreGeneratorCategory::STATUS => GeneratorCategory::STATUS,
CoreGeneratorCategory::METADATA => GeneratorCategory::METADATA
}
}
}
impl From<GeneratorCategory> for CoreGeneratorCategory {
#[inline]
fn from(category: GeneratorCategory) -> CoreGeneratorCategory {
match category {
GeneratorCategory::METHOD => CoreGeneratorCategory::METHOD,
GeneratorCategory::PATH => CoreGeneratorCategory::PATH,
GeneratorCategory::HEADER => CoreGeneratorCategory::HEADER,
GeneratorCategory::QUERY => CoreGeneratorCategory::QUERY,
GeneratorCategory::BODY => CoreGeneratorCategory::BODY,
GeneratorCategory::STATUS => CoreGeneratorCategory::STATUS,
GeneratorCategory::METADATA => CoreGeneratorCategory::METADATA
}
}
}
#[derive(Debug)]
pub struct GeneratorCategoryIterator {
generators: Vec<(DocPath, Generator)>,
current_idx: usize
}
impl GeneratorCategoryIterator {
fn new(generators: &HashMap<DocPath, Generator>) -> GeneratorCategoryIterator {
let generators = generators.iter()
.sorted_by(|(a, _), (b, _)| Ord::cmp(a.to_string().as_str(), b.to_string().as_str()))
.map(|(k, v)| (k.clone(), v.clone()));
GeneratorCategoryIterator {
generators: generators.collect(),
current_idx: 0
}
}
pub fn new_from_contents(contents: &MessageContents, category: GeneratorCategory) -> Self {
let category: CoreGeneratorCategory = category.into();
let empty = hashmap!{};
GeneratorCategoryIterator::new(contents.generators.categories.get(&category).unwrap_or(&empty))
}
pub fn new_from_request(request: &HttpRequest, category: GeneratorCategory) -> Self {
let category: CoreGeneratorCategory = category.into();
let empty = hashmap!{};
GeneratorCategoryIterator::new(request.generators.categories.get(&category).unwrap_or(&empty))
}
pub fn new_from_response(response: &HttpResponse, category: GeneratorCategory) -> Self {
let category: CoreGeneratorCategory = category.into();
let empty = hashmap!{};
GeneratorCategoryIterator::new(response.generators.categories.get(&category).unwrap_or(&empty))
}
fn next(&mut self) -> Option<&(DocPath, Generator)> {
let value = self.generators.get(self.current_idx);
self.current_idx += 1;
value
}
}
ffi_fn! {
fn pactffi_generators_iter_delete(iter: *mut GeneratorCategoryIterator) {
ptr::drop_raw(iter);
}
}
#[derive(Debug)]
#[repr(C)]
pub struct GeneratorKeyValuePair {
pub path: *const c_char,
pub generator: *const Generator
}
impl GeneratorKeyValuePair {
fn new(
key: &str,
value: &Generator
) -> anyhow::Result<GeneratorKeyValuePair> {
Ok(GeneratorKeyValuePair {
path: string::to_c(key)? as *const c_char,
generator: raw_to(value.clone()) as *const Generator
})
}
}
impl Drop for GeneratorKeyValuePair {
fn drop(&mut self) {
string::pactffi_string_delete(self.path as *mut c_char);
drop_raw(self.generator as *mut Generator);
}
}
ffi_fn! {
fn pactffi_generators_iter_next(iter: *mut GeneratorCategoryIterator) -> *const GeneratorKeyValuePair {
let iter = as_mut!(iter);
match iter.next() {
Some((path, generator)) => {
let pair = GeneratorKeyValuePair::new(&path.to_string(), generator)?;
ptr::raw_to(pair)
}
None => {
trace!("iter past the end of the generators");
std::ptr::null_mut()
}
}
} {
std::ptr::null_mut()
}
}
ffi_fn! {
fn pactffi_generators_iter_pair_delete(pair: *const GeneratorKeyValuePair) {
ptr::drop_raw(pair as *mut GeneratorKeyValuePair);
}
}
#[cfg(test)]
mod tests {
use std::ffi::CString;
use expectest::prelude::*;
use libc::c_char;
use pact_models::generators::Generator;
use pact_models::prelude::Generator::{RandomInt, RandomString};
use crate::models::generators::{
pactffi_generator_generate_integer,
pactffi_generator_generate_string,
pactffi_generator_to_json
};
use crate::util::string;
#[test]
fn generate_string_test() {
let generator = RandomString(4);
let value = pactffi_generator_generate_string(&generator, std::ptr::null());
expect!(value.is_null()).to(be_false());
let string = unsafe { CString::from_raw(value as *mut c_char) };
expect!(string.to_string_lossy().len()).to(be_equal_to(4));
}
#[test]
fn generate_string_test_with_invalid_context() {
let generator = RandomString(4);
let context = "{not valid";
let context_json = string::to_c(context).unwrap();
let value = pactffi_generator_generate_string(&generator, context_json);
expect!(value.is_null()).to(be_true());
}
#[test]
fn generate_integer_test() {
let generator = RandomInt(10, 100);
let value = pactffi_generator_generate_integer(&generator, std::ptr::null());
expect!(value).to(be_greater_or_equal_to(10));
expect!(value).to(be_less_or_equal_to(100));
}
#[test]
fn generator_json() {
let generator = RandomInt(10, 100);
let generator_ptr = &generator as *const Generator;
let json_ptr = pactffi_generator_to_json(generator_ptr);
let json = unsafe { CString::from_raw(json_ptr as *mut c_char) };
expect!(json.to_string_lossy()).to(be_equal_to("{\"max\":100,\"min\":10,\"type\":\"RandomInt\"}"));
}
}