car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrappers for `car-nlp` primitives (EPIC F / F4).
//!
//! Stateless, side-effect-free: they run the NLP backend in-process (Apple
//! NaturalLanguage on macOS, the pure-Rust fallback elsewhere) and return JSON,
//! so the daemon `nlp.*` WS methods and the NAPI/PyO3 free functions share one
//! implementation. Every result carries `backend` (`"apple"` | `"fallback"`) so
//! a caller knows which quality tier answered.

use serde_json::json;

/// `nlp.identify_language` — BCP-47/ISO language code of the dominant language,
/// or `null` when undetermined. Returns `{ language, backend }` JSON.
pub fn identify_language(text: &str) -> Result<String, String> {
    let lang = car_nlp::identify_language(text).map_err(|e| e.to_string())?;
    Ok(json!({ "language": lang, "backend": car_nlp::backend_name() }).to_string())
}

/// `nlp.tokenize` — word-level tokens. Returns `{ tokens, backend }` JSON.
pub fn tokenize_words(text: &str) -> Result<String, String> {
    let tokens = car_nlp::tokenize_words(text).map_err(|e| e.to_string())?;
    Ok(json!({ "tokens": tokens, "backend": car_nlp::backend_name() }).to_string())
}

/// `nlp.extract_entities` — named entities (`{text, kind, byte_range}`).
/// Returns `{ entities, backend }` JSON. The fallback reports `kind:"entity"`
/// (no person/place/org classification); Apple reports the typed category.
pub fn extract_named_entities(text: &str) -> Result<String, String> {
    let entities = car_nlp::extract_named_entities(text).map_err(|e| e.to_string())?;
    Ok(json!({ "entities": entities, "backend": car_nlp::backend_name() }).to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wrappers_return_backend_tagged_json() {
        let v: serde_json::Value =
            serde_json::from_str(&identify_language("hello world this is english").unwrap())
                .unwrap();
        assert!(v.get("language").is_some());
        assert!(v.get("backend").is_some());

        let v: serde_json::Value =
            serde_json::from_str(&tokenize_words("hello world").unwrap()).unwrap();
        assert_eq!(v["tokens"].as_array().unwrap().len(), 2);

        let v: serde_json::Value =
            serde_json::from_str(&extract_named_entities("I visited Paris").unwrap()).unwrap();
        assert!(v.get("entities").is_some());
    }
}