use crate::status::KgliteStatusCode;
use crate::strings::alloc_c_string;
use kglite::api::blueprint::{build as blueprint_build, load_blueprint_file};
use kglite::api::introspection::{compute_schema, schema_overview_to_json};
use kglite::api::io::{load_file, load_kgl_bytes, save_graph, save_graph_with, write_kgl_to};
#[cfg(feature = "rdf")]
use kglite::api::io::{load_rdf as core_load_rdf, RdfConfig};
use kglite::api::storage::{new_dir_graph_in_mode, StorageMode};
use kglite::api::{graphgen, DirGraph, GraphGenConfig};
use std::ffi::{c_char, CStr};
use std::path::Path;
use std::sync::Arc;
#[repr(C)]
pub struct KgliteGraph {
_opaque: [u8; 0],
_marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
pub(crate) struct GraphState {
pub(crate) inner: Arc<DirGraph>,
}
impl GraphState {
pub(crate) fn into_handle(arc: Arc<DirGraph>) -> *mut KgliteGraph {
let boxed = Box::new(GraphState { inner: arc });
Box::into_raw(boxed).cast::<KgliteGraph>()
}
pub(crate) unsafe fn from_handle_mut<'a>(handle: *mut KgliteGraph) -> &'a mut GraphState {
unsafe { &mut *handle.cast::<GraphState>() }
}
pub(crate) unsafe fn free_handle(handle: *mut KgliteGraph) {
if handle.is_null() {
return;
}
let _ = unsafe { Box::from_raw(handle.cast::<GraphState>()) };
}
}
#[no_mangle]
pub extern "C" fn kglite_graph_new() -> *mut KgliteGraph {
crate::ffi::value_boundary(std::ptr::null_mut(), || {
GraphState::into_handle(Arc::new(DirGraph::new()))
})
}
#[no_mangle]
pub unsafe extern "C" fn kglite_graph_new_in_mode(
mode: *const c_char,
path: *const c_char,
out_graph: *mut *mut KgliteGraph,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_graph, std::ptr::null_mut()),
|| {
if mode.is_null() || out_graph.is_null() {
return KgliteStatusCode::NullPointer;
}
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let path_opt: Option<&str> = if path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return KgliteStatusCode::InvalidUtf8,
}
};
let sm = match StorageMode::parse(mode_str) {
Ok(m) => m,
Err(msg) => {
return fail_new_in_mode(KgliteStatusCode::InvalidArgument, &msg, out_error_msg)
}
};
if matches!(sm, StorageMode::Disk) && path_opt.is_none() {
return fail_new_in_mode(
KgliteStatusCode::InvalidArgument,
"storage mode 'disk' requires a directory path",
out_error_msg,
);
}
match new_dir_graph_in_mode(sm, path_opt.map(Path::new)) {
Ok(graph) => {
unsafe {
*out_graph = GraphState::into_handle(Arc::new(graph));
if !out_error_msg.is_null() {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(msg) => fail_new_in_mode(KgliteStatusCode::FileIo, &msg, out_error_msg),
}
},
)
}
fn fail_new_in_mode(
code: KgliteStatusCode,
msg: &str,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(msg);
}
}
code
}
#[no_mangle]
pub unsafe extern "C" fn kglite_load_file(
path: *const c_char,
out_graph: *mut *mut KgliteGraph,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_graph, std::ptr::null_mut()),
|| {
if path.is_null() || out_graph.is_null() {
return KgliteStatusCode::NullPointer;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
match load_file(path_str) {
Ok(arc) => {
unsafe {
*out_graph = GraphState::into_handle(arc);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(io_err) => {
unsafe {
*out_graph = std::ptr::null_mut();
}
let (code, message) = classify_io_error(&io_err);
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&message);
}
}
code
}
}
},
)
}
fn classify_io_error(err: &std::io::Error) -> (KgliteStatusCode, String) {
let code = match err.kind() {
std::io::ErrorKind::NotFound => KgliteStatusCode::FileNotFound,
std::io::ErrorKind::InvalidData => KgliteStatusCode::FileFormat,
_ => KgliteStatusCode::FileIo,
};
(code, err.to_string())
}
#[cfg(feature = "rdf")]
#[no_mangle]
pub unsafe extern "C" fn kglite_load_rdf(
path: *const c_char,
languages_json: *const c_char,
label_predicates_json: *const c_char,
keep_full_iris: u8,
default_type: *const c_char,
max_triples: i64,
out_graph: *mut *mut KgliteGraph,
out_stats_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| {
crate::ffi::init_out(out_graph, std::ptr::null_mut());
crate::ffi::init_out(out_stats_json, std::ptr::null());
},
|| {
use std::collections::HashSet;
if path.is_null() || out_graph.is_null() {
return KgliteStatusCode::NullPointer;
}
let set_err = |msg: &str| {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(msg);
}
}
};
let cstr_opt = |p: *const c_char| -> Result<Option<&str>, ()> {
if p.is_null() {
Ok(None)
} else {
unsafe { CStr::from_ptr(p) }
.to_str()
.map(Some)
.map_err(|_| ())
}
};
let path_str = match cstr_opt(path) {
Ok(Some(s)) => s,
Ok(None) => unreachable!("path null-checked above"),
Err(()) => return KgliteStatusCode::InvalidUtf8,
};
let languages = match cstr_opt(languages_json) {
Ok(None) => None,
Ok(Some(s)) => match serde_json::from_str::<Vec<String>>(s) {
Ok(v) => Some(v.into_iter().collect::<HashSet<_>>()),
Err(e) => {
set_err(&format!(
"languages_json must be a JSON array of strings: {e}"
));
return KgliteStatusCode::InvalidArgument;
}
},
Err(()) => return KgliteStatusCode::InvalidUtf8,
};
let label_predicates = match cstr_opt(label_predicates_json) {
Ok(None) => vec!["http://www.w3.org/2000/01/rdf-schema#label".to_string()],
Ok(Some(s)) => match serde_json::from_str::<Vec<String>>(s) {
Ok(v) => v,
Err(e) => {
set_err(&format!(
"label_predicates_json must be a JSON array of strings: {e}"
));
return KgliteStatusCode::InvalidArgument;
}
},
Err(()) => return KgliteStatusCode::InvalidUtf8,
};
let default_type = match cstr_opt(default_type) {
Ok(Some(s)) => s.to_string(),
Ok(None) => "Resource".to_string(),
Err(()) => return KgliteStatusCode::InvalidUtf8,
};
let config = RdfConfig {
languages,
label_predicates,
keep_full_iris: keep_full_iris != 0,
default_type,
max_triples: if max_triples < 0 {
None
} else {
Some(max_triples as u64)
},
};
let mut graph = DirGraph::new();
match core_load_rdf(&mut graph, path_str, &config) {
Ok(stats) => {
unsafe {
*out_graph = GraphState::into_handle(Arc::new(graph));
}
if !out_stats_json.is_null() {
let json = serde_json::json!({
"nodes": stats.nodes_created,
"edges": stats.edges_created,
"triples": stats.triples_processed,
})
.to_string();
unsafe {
*out_stats_json = alloc_c_string(&json);
}
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(msg) => {
let code = classify_rdf_error(&msg);
set_err(&msg);
code
}
}
},
)
}
#[cfg(feature = "rdf")]
fn classify_rdf_error(msg: &str) -> KgliteStatusCode {
if msg.starts_with("Cannot open") {
KgliteStatusCode::FileNotFound
} else if msg.starts_with("Unsupported RDF extension") {
KgliteStatusCode::InvalidArgument
} else if msg.contains("parse error") {
KgliteStatusCode::FileFormat
} else {
KgliteStatusCode::InvalidArgument
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_save_graph(
graph: *mut KgliteGraph,
path: *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| {},
|| {
if graph.is_null() || path.is_null() {
return KgliteStatusCode::NullPointer;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let state = unsafe { GraphState::from_handle_mut(graph) };
match save_graph(&mut state.inner, path_str) {
Ok(()) => {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(msg) => {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&msg);
}
}
KgliteStatusCode::FileIo
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_graph_free(graph: *mut KgliteGraph) {
crate::ffi::void_boundary(|| {
unsafe { GraphState::free_handle(graph) };
});
}
#[no_mangle]
pub unsafe extern "C" fn kglite_graphgen_to_dir(
persons: u64,
knows_per: u64,
seed: u64,
zipf: u8,
zipf_exp: f64,
out_dir: *const c_char,
out_stats_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_stats_json, std::ptr::null()),
|| {
if out_dir.is_null() || out_stats_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let dir = match unsafe { CStr::from_ptr(out_dir) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let cfg = GraphGenConfig {
persons,
knows_per,
seed,
zipf: zipf != 0,
zipf_exp,
};
match graphgen(&cfg, Path::new(dir)) {
Ok(stats) => {
let json =
serde_json::json!({"nodes": stats.nodes, "edges": stats.edges}).to_string();
unsafe {
*out_stats_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(e) => {
unsafe {
*out_stats_json = std::ptr::null();
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&e.to_string());
}
}
KgliteStatusCode::FileIo
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_blueprint_build(
blueprint_path: *const c_char,
csv_dir: *const c_char,
out_graph: *mut *mut KgliteGraph,
out_report_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| {
crate::ffi::init_out(out_graph, std::ptr::null_mut());
crate::ffi::init_out(out_report_json, std::ptr::null());
},
|| {
if blueprint_path.is_null()
|| csv_dir.is_null()
|| out_graph.is_null()
|| out_report_json.is_null()
{
return KgliteStatusCode::NullPointer;
}
let bp_path = match unsafe { CStr::from_ptr(blueprint_path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let dir = match unsafe { CStr::from_ptr(csv_dir) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let set_err = |out_error_msg: *mut *const c_char, msg: &str| {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(msg);
}
}
};
let blueprint = match load_blueprint_file(Path::new(bp_path)) {
Ok(b) => b,
Err(e) => {
unsafe {
*out_graph = std::ptr::null_mut();
*out_report_json = std::ptr::null();
}
set_err(out_error_msg, &e);
return KgliteStatusCode::FileFormat;
}
};
let mut graph = DirGraph::new();
let report = match blueprint_build(&mut graph, blueprint, Path::new(dir)) {
Ok(r) => r,
Err(e) => {
unsafe {
*out_graph = std::ptr::null_mut();
*out_report_json = std::ptr::null();
}
set_err(out_error_msg, &e);
return KgliteStatusCode::InvalidArgument;
}
};
unsafe {
*out_graph = GraphState::into_handle(Arc::new(graph));
}
let report_json = serde_json::json!({
"nodes_by_type": report.nodes_by_type,
"edges_by_type": report.edges_by_type,
"warnings": report.warnings,
"errors": report.errors,
"provisional_purged": report.provisional_purged,
})
.to_string();
unsafe {
*out_report_json = alloc_c_string(&report_json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_save_graph_durable(
graph: *mut KgliteGraph,
path: *const c_char,
fsync: u8,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| {},
|| {
if graph.is_null() || path.is_null() {
return KgliteStatusCode::NullPointer;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let state = unsafe { GraphState::from_handle_mut(graph) };
match save_graph_with(&mut state.inner, path_str, fsync != 0) {
Ok(()) => {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(msg) => {
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&msg);
}
}
KgliteStatusCode::FileIo
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_graph_to_bytes(
graph: *mut KgliteGraph,
out_buf: *mut *mut u8,
out_len: *mut usize,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| {
crate::ffi::init_out(out_buf, std::ptr::null_mut());
crate::ffi::init_out(out_len, 0);
},
|| {
if graph.is_null() || out_buf.is_null() || out_len.is_null() {
return KgliteStatusCode::NullPointer;
}
let state = unsafe { GraphState::from_handle_mut(graph) };
let mut buf: Vec<u8> = Vec::new();
match write_kgl_to(state.inner.as_ref(), &mut buf) {
Ok(()) => {
let boxed: Box<[u8]> = buf.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *mut u8;
unsafe {
*out_buf = ptr;
*out_len = len;
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(e) => {
unsafe {
*out_buf = std::ptr::null_mut();
*out_len = 0;
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&e.to_string());
}
}
KgliteStatusCode::FileIo
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_free_bytes(buf: *mut u8, len: usize) {
crate::ffi::void_boundary(|| {
if buf.is_null() {
return;
}
let _ = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(buf, len)) };
});
}
#[no_mangle]
pub unsafe extern "C" fn kglite_graph_from_bytes(
data: *const u8,
len: usize,
out_graph: *mut *mut KgliteGraph,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_graph, std::ptr::null_mut()),
|| {
if data.is_null() || out_graph.is_null() {
return KgliteStatusCode::NullPointer;
}
let slice = unsafe { std::slice::from_raw_parts(data, len) };
match load_kgl_bytes(slice) {
Ok(arc) => {
unsafe {
*out_graph = GraphState::into_handle(arc);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(e) => {
unsafe {
*out_graph = std::ptr::null_mut();
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&e.to_string());
}
}
KgliteStatusCode::FileFormat
}
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn kglite_compute_schema_json(
graph: *mut KgliteGraph,
out_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
crate::ffi::status_boundary(
out_error_msg,
|| crate::ffi::init_out(out_json, std::ptr::null()),
|| {
if graph.is_null() || out_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let state = unsafe { GraphState::from_handle_mut(graph) };
let schema = compute_schema(state.inner.as_ref());
let json = schema_overview_to_json(&schema).to_string();
unsafe {
*out_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn load_nonexistent_file_returns_file_not_found() {
let path = CString::new("/tmp/__kglite_c_does_not_exist__.kgl").unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc =
unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::FileNotFound);
assert!(graph.is_null());
assert!(!err.is_null());
unsafe { crate::kglite_free_string(err) };
}
#[test]
fn load_null_path_returns_null_pointer() {
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc =
unsafe { kglite_load_file(std::ptr::null(), &mut graph as *mut _, &mut err as *mut _) };
assert_eq!(rc, KgliteStatusCode::NullPointer);
}
#[test]
fn graph_free_is_null_safe() {
unsafe { kglite_graph_free(std::ptr::null_mut()) };
}
#[test]
fn graph_new_returns_non_null_and_frees() {
let g = kglite_graph_new();
assert!(!g.is_null());
unsafe { kglite_graph_free(g) };
}
fn new_in_mode(mode: &str, path: Option<&str>) -> (KgliteStatusCode, *mut KgliteGraph, String) {
let mode_c = CString::new(mode).unwrap();
let path_c = path.map(|p| CString::new(p).unwrap());
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_graph_new_in_mode(
mode_c.as_ptr(),
path_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
&mut graph as *mut _,
&mut err as *mut _,
)
};
let msg = if err.is_null() {
String::new()
} else {
let s = unsafe { CStr::from_ptr(err).to_str().unwrap().to_string() };
unsafe { crate::kglite_free_string(err) };
s
};
(rc, graph, msg)
}
#[test]
fn graph_new_in_mode_memory_and_mapped() {
for mode in ["memory", "default", "mapped"] {
let (rc, g, _) = new_in_mode(mode, None);
assert_eq!(rc, KgliteStatusCode::Ok, "mode {mode}");
assert!(!g.is_null(), "mode {mode}");
unsafe { kglite_graph_free(g) };
}
}
#[test]
fn graph_new_in_mode_disk_creates_at_path() {
let dir = std::env::temp_dir().join(format!("kglite_c_newmode_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let (rc, g, msg) = new_in_mode("disk", Some(dir.to_str().unwrap()));
assert_eq!(rc, KgliteStatusCode::Ok, "{msg}");
assert!(!g.is_null());
unsafe { kglite_graph_free(g) };
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn graph_new_in_mode_unknown_is_invalid_argument() {
let (rc, g, msg) = new_in_mode("nope", None);
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
assert!(g.is_null());
assert!(msg.contains("Unknown storage mode"));
}
#[test]
fn graph_new_in_mode_disk_without_path_is_invalid_argument() {
let (rc, g, msg) = new_in_mode("disk", None);
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
assert!(g.is_null());
assert!(msg.contains("requires a directory path"));
}
#[test]
fn graph_new_in_mode_null_returns_null_pointer() {
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_graph_new_in_mode(
std::ptr::null(),
std::ptr::null(),
&mut graph as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::NullPointer);
}
#[test]
fn graphgen_writes_stats() {
let dir =
std::env::temp_dir().join(format!("kglite_c_graphgen_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let dir_c = CString::new(dir.to_str().unwrap()).unwrap();
let mut stats: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_graphgen_to_dir(
50,
3,
42,
0,
1.2,
dir_c.as_ptr(),
&mut stats as *mut _,
&mut err as *mut _,
)
};
let error = (!err.is_null()).then(|| unsafe { CStr::from_ptr(err).to_string_lossy() });
assert_eq!(rc, KgliteStatusCode::Ok, "{error:?}");
assert!(!stats.is_null());
let s = unsafe { CStr::from_ptr(stats).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
assert!(parsed["nodes"].as_u64().unwrap() > 0);
assert!(parsed["edges"].as_u64().unwrap() > 0);
unsafe { crate::kglite_free_string(stats) };
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn blueprint_build_missing_file_returns_file_format() {
let bp = CString::new("/tmp/__kglite_c_no_blueprint__.json").unwrap();
let dir = CString::new("/tmp").unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut report: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_blueprint_build(
bp.as_ptr(),
dir.as_ptr(),
&mut graph as *mut _,
&mut report as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::FileFormat);
assert!(graph.is_null());
assert!(!err.is_null());
unsafe { crate::kglite_free_string(err) };
}
#[cfg(feature = "rdf")]
#[test]
fn load_rdf_turtle_builds_graph() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("kglite_c_rdf_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("g.ttl");
{
let mut f = std::fs::File::create(&p).unwrap();
write!(
f,
"@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\
<http://ex/a> a foaf:Person ; rdfs:label \"A\" ; foaf:knows <http://ex/b> .\n\
<http://ex/b> a foaf:Person ; rdfs:label \"B\" .\n"
)
.unwrap();
}
let path_c = CString::new(p.to_str().unwrap()).unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut stats: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_load_rdf(
path_c.as_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
std::ptr::null(),
-1,
&mut graph as *mut _,
&mut stats as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
assert!(!graph.is_null());
assert!(!stats.is_null());
let s = unsafe { CStr::from_ptr(stats).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
assert_eq!(parsed["nodes"].as_u64().unwrap(), 2);
assert_eq!(parsed["edges"].as_u64().unwrap(), 1);
unsafe { crate::kglite_free_string(stats) };
unsafe { kglite_graph_free(graph) };
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "rdf")]
#[test]
fn load_rdf_missing_file_returns_file_not_found() {
let path_c = CString::new("/tmp/__kglite_c_no_rdf__.ttl").unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_load_rdf(
path_c.as_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
std::ptr::null(),
-1,
&mut graph as *mut _,
std::ptr::null_mut(),
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::FileNotFound);
assert!(graph.is_null());
assert!(!err.is_null());
unsafe { crate::kglite_free_string(err) };
}
#[cfg(feature = "rdf")]
#[test]
fn load_rdf_bad_extension_is_invalid_argument() {
let dir = std::env::temp_dir().join(format!("kglite_c_rdfext_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("g.rdfxml");
std::fs::write(&p, b"x").unwrap();
let path_c = CString::new(p.to_str().unwrap()).unwrap();
let mut graph: *mut KgliteGraph = std::ptr::null_mut();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_load_rdf(
path_c.as_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
std::ptr::null(),
-1,
&mut graph as *mut _,
std::ptr::null_mut(),
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
assert!(graph.is_null());
assert!(!err.is_null());
unsafe { crate::kglite_free_string(err) };
let _ = std::fs::remove_dir_all(&dir);
}
}