use crate::strings::alloc_c_string;
use kglite::api::cypher::CypherResult;
use kglite::api::Value;
use std::ffi::c_char;
#[repr(C)]
pub struct KgliteCypherResult {
_opaque: [u8; 0],
_marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
pub(crate) struct ResultState {
pub(crate) inner: CypherResult,
}
impl ResultState {
pub(crate) fn into_handle(result: CypherResult) -> *mut KgliteCypherResult {
let boxed = Box::new(ResultState { inner: result });
Box::into_raw(boxed).cast::<KgliteCypherResult>()
}
unsafe fn from_handle<'a>(handle: *const KgliteCypherResult) -> &'a ResultState {
unsafe { &*handle.cast::<ResultState>() }
}
unsafe fn free_handle(handle: *mut KgliteCypherResult) {
if handle.is_null() {
return;
}
let _ = unsafe { Box::from_raw(handle.cast::<ResultState>()) };
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_cypher_result_columns_json(
result: *const KgliteCypherResult,
) -> *const c_char {
if result.is_null() {
return std::ptr::null();
}
let state = unsafe { ResultState::from_handle(result) };
match serde_json::to_string(&state.inner.columns) {
Ok(s) => alloc_c_string(&s),
Err(_) => std::ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_cypher_result_rows_json(
result: *const KgliteCypherResult,
) -> *const c_char {
if result.is_null() {
return std::ptr::null();
}
let state = unsafe { ResultState::from_handle(result) };
let cypher_result = &state.inner;
let mut rows = Vec::with_capacity(cypher_result.rows.len());
for row in &cypher_result.rows {
let mut obj = serde_json::Map::with_capacity(cypher_result.columns.len());
for (idx, col) in cypher_result.columns.iter().enumerate() {
let cell = row.get(idx).unwrap_or(&Value::Null);
let json_val = match serde_json::to_value(cell) {
Ok(v) => v,
Err(_) => serde_json::Value::Null,
};
obj.insert(col.clone(), json_val);
}
rows.push(serde_json::Value::Object(obj));
}
match serde_json::to_string(&rows) {
Ok(s) => alloc_c_string(&s),
Err(_) => std::ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_cypher_result_row_count(
result: *const KgliteCypherResult,
) -> usize {
if result.is_null() {
return 0;
}
let state = unsafe { ResultState::from_handle(result) };
state.inner.rows.len()
}
#[no_mangle]
pub unsafe extern "C" fn kglite_cypher_result_free(result: *mut KgliteCypherResult) {
unsafe { ResultState::free_handle(result) };
}
#[cfg(test)]
mod tests {
use super::*;
use kglite::api::Value;
fn fixture_result() -> *mut KgliteCypherResult {
let r = CypherResult {
columns: vec!["name".to_string(), "age".to_string()],
rows: vec![
vec![Value::String("alice".into()), Value::Int64(30)],
vec![Value::String("bob".into()), Value::Int64(25)],
],
stats: None,
profile: None,
diagnostics: None,
lazy: None,
};
ResultState::into_handle(r)
}
#[test]
fn columns_json_round_trips() {
let r = fixture_result();
let json_ptr = unsafe { kglite_cypher_result_columns_json(r) };
assert!(!json_ptr.is_null());
let s = unsafe { std::ffi::CStr::from_ptr(json_ptr).to_str().unwrap() };
assert_eq!(s, r#"["name","age"]"#);
unsafe { crate::kglite_free_string(json_ptr) };
unsafe { kglite_cypher_result_free(r) };
}
#[test]
fn rows_json_round_trips() {
let r = fixture_result();
let json_ptr = unsafe { kglite_cypher_result_rows_json(r) };
assert!(!json_ptr.is_null());
let s = unsafe { std::ffi::CStr::from_ptr(json_ptr).to_str().unwrap() };
assert!(s.contains("alice"));
assert!(s.contains("bob"));
unsafe { crate::kglite_free_string(json_ptr) };
unsafe { kglite_cypher_result_free(r) };
}
#[test]
fn row_count_matches() {
let r = fixture_result();
let n = unsafe { kglite_cypher_result_row_count(r) };
assert_eq!(n, 2);
unsafe { kglite_cypher_result_free(r) };
}
#[test]
fn null_safe_accessors() {
assert!(unsafe { kglite_cypher_result_columns_json(std::ptr::null()) }.is_null());
assert!(unsafe { kglite_cypher_result_rows_json(std::ptr::null()) }.is_null());
assert_eq!(
unsafe { kglite_cypher_result_row_count(std::ptr::null()) },
0
);
unsafe { kglite_cypher_result_free(std::ptr::null_mut()) };
}
}