use crate::status::KgliteStatusCode;
use crate::strings::alloc_c_string;
use kglite::api::datasets::wikidata::{
decide, ensure_dump_blocking, remote_last_modified_blocking, CacheDecision, FreshnessInputs,
Workdir,
};
use std::ffi::{c_char, CStr};
use std::path::Path;
#[no_mangle]
pub unsafe extern "C" fn kglite_datasets_wikidata_ensure_dump(
workdir_path: *const c_char,
cooldown_days: i64,
verbose: u8,
out_dump_path: *mut *const c_char,
out_remote_mtime_iso: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
if workdir_path.is_null() || out_dump_path.is_null() {
return KgliteStatusCode::NullPointer;
}
let workdir_str = match unsafe { CStr::from_ptr(workdir_path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let workdir = Workdir::new(workdir_str);
match ensure_dump_blocking(&workdir, cooldown_days, verbose != 0) {
Ok((dump_path, remote_mtime)) => {
let path_str = dump_path.to_string_lossy().to_string();
unsafe {
*out_dump_path = alloc_c_string(&path_str);
}
if !out_remote_mtime_iso.is_null() {
unsafe {
*out_remote_mtime_iso = match remote_mtime {
Some(dt) => alloc_c_string(&dt.to_rfc3339()),
None => std::ptr::null(),
};
}
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
Err(err) => {
unsafe {
*out_dump_path = std::ptr::null();
}
if !out_remote_mtime_iso.is_null() {
unsafe {
*out_remote_mtime_iso = std::ptr::null();
}
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = alloc_c_string(&err.to_string());
}
}
KgliteStatusCode::Internal
}
}
}
#[no_mangle]
pub unsafe extern "C" fn kglite_datasets_wikidata_remote_last_modified(
out_iso: *mut *const c_char,
) -> KgliteStatusCode {
if out_iso.is_null() {
return KgliteStatusCode::NullPointer;
}
match remote_last_modified_blocking() {
Some(dt) => unsafe {
*out_iso = alloc_c_string(&dt.to_rfc3339());
},
None => unsafe {
*out_iso = std::ptr::null();
},
}
KgliteStatusCode::Ok
}
#[no_mangle]
pub unsafe extern "C" fn kglite_datasets_wikidata_decide_cache(
force_rebuild: u8,
graph_meta_path: *const c_char,
source_meta_path: *const c_char,
cooldown_days: i64,
remote_mtime_iso: *const c_char,
out_decision_json: *mut *const c_char,
out_error_msg: *mut *const c_char,
) -> KgliteStatusCode {
if graph_meta_path.is_null() || source_meta_path.is_null() || out_decision_json.is_null() {
return KgliteStatusCode::NullPointer;
}
let graph_meta_str = match unsafe { CStr::from_ptr(graph_meta_path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let source_meta_str = match unsafe { CStr::from_ptr(source_meta_path) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
let remote_mtime = if remote_mtime_iso.is_null() {
None
} else {
let s = match unsafe { CStr::from_ptr(remote_mtime_iso) }.to_str() {
Ok(s) => s,
Err(_) => return KgliteStatusCode::InvalidUtf8,
};
match chrono::DateTime::parse_from_rfc3339(s) {
Ok(dt) => Some(dt.with_timezone(&chrono::Utc)),
Err(_) => return KgliteStatusCode::InvalidArgument,
}
};
let inputs = FreshnessInputs {
force_rebuild: force_rebuild != 0,
graph_meta_path: Path::new(graph_meta_str),
source_meta_path: Path::new(source_meta_str),
cooldown_days,
remote_mtime,
};
let decision = decide(inputs);
let json = serialize_decision(&decision);
unsafe {
*out_decision_json = alloc_c_string(&json);
}
if !out_error_msg.is_null() {
unsafe {
*out_error_msg = std::ptr::null();
}
}
KgliteStatusCode::Ok
}
fn serialize_decision(decision: &CacheDecision) -> String {
match decision {
CacheDecision::Build { reason } => serde_json::json!({
"decision": "build",
"reason": reason,
})
.to_string(),
CacheDecision::Load { reason } => serde_json::json!({
"decision": "load",
"reason": reason,
})
.to_string(),
CacheDecision::Rebuild { reason } => serde_json::json!({
"decision": "rebuild",
"reason": reason,
})
.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn decide_cache_force_rebuild_returns_build() {
let graph_meta = CString::new("/tmp/does_not_exist/disk_graph_meta.json").unwrap();
let source_meta = CString::new("/tmp/does_not_exist/wikidata_source.json").unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_datasets_wikidata_decide_cache(
1,
graph_meta.as_ptr(),
source_meta.as_ptr(),
7,
std::ptr::null(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
let s = unsafe { CStr::from_ptr(out).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
assert_eq!(parsed["decision"], "build");
assert_eq!(parsed["reason"], "force_rebuild");
unsafe { crate::kglite_free_string(out) };
}
#[test]
fn decide_cache_missing_meta_returns_build_no_cache() {
let graph_meta = CString::new("/tmp/kglite_c_wikidata_test_missing").unwrap();
let source_meta = CString::new("/tmp/kglite_c_wikidata_test_missing_src").unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_datasets_wikidata_decide_cache(
0,
graph_meta.as_ptr(),
source_meta.as_ptr(),
7,
std::ptr::null(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::Ok);
let s = unsafe { CStr::from_ptr(out).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
assert_eq!(parsed["decision"], "build");
assert_eq!(parsed["reason"], "no_cache");
unsafe { crate::kglite_free_string(out) };
}
#[test]
fn decide_cache_bad_remote_mtime_returns_invalid_argument() {
let graph_meta = CString::new("/tmp/anywhere").unwrap();
let source_meta = CString::new("/tmp/anywhere").unwrap();
let bad_iso = CString::new("not-a-timestamp").unwrap();
let mut out: *const c_char = std::ptr::null();
let mut err: *const c_char = std::ptr::null();
let rc = unsafe {
kglite_datasets_wikidata_decide_cache(
0,
graph_meta.as_ptr(),
source_meta.as_ptr(),
7,
bad_iso.as_ptr(),
&mut out as *mut _,
&mut err as *mut _,
)
};
assert_eq!(rc, KgliteStatusCode::InvalidArgument);
}
}