car-ffi-common 0.15.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! In-process A2UI surface store for FFI bindings.
//!
//! Wraps `car_a2ui::A2uiSurfaceStore` behind a process-singleton so
//! NAPI/PyO3 consumers get a stable handle without re-instantiating
//! the store per call. Each binding exposes thin JSON-string wrappers
//! that delegate here.
//!
//! Scope: in-process only. The daemon's WebSocket A2UI methods
//! (`a2ui.apply` etc.) keep their own `A2uiSurfaceStore` instance —
//! this helper does NOT proxy to the daemon. Embedded callers that
//! want their own surface state use these; daemon-shared state still
//! flows over WebSocket.
//!
//! ## Wire shapes
//!
//! All functions take JSON strings and return JSON strings. The
//! envelope/result types are `car_a2ui::A2uiEnvelope` /
//! `A2uiApplyResult` etc. — the same shapes the WebSocket handler
//! accepts and emits, so a host can move between transports without
//! reshaping payloads.
//!
//! - `capabilities()` → `A2uiCapabilities`
//! - `apply(envelope_json)` → `A2uiApplyResult`
//! - `ingest(payload_json)` → `{ "applied": [A2uiApplyResult] }` —
//!   parses A2UI envelopes from common A2A carrier shapes (`a2ui`
//!   key, `data` parts, `artifact` payloads) and applies each one
//!   in order. Owner is auto-extracted from A2A task/context fields
//!   when present.
//! - `surfaces()` → `[A2uiSurface]`
//! - `get(surface_id)` → `A2uiSurface | null`
//! - `reap()` → `{ "removed": [surface_id] }`
//! - `validate_payload(value_json)` → `null` on success; error message
//!   string on failure (limit-exceeded, parse error).

use std::sync::OnceLock;

use car_a2ui::{A2uiEnvelope, A2uiSurfaceStore};
use serde_json::Value;

fn store() -> &'static A2uiSurfaceStore {
    static STORE: OnceLock<A2uiSurfaceStore> = OnceLock::new();
    STORE.get_or_init(A2uiSurfaceStore::new)
}

pub fn capabilities() -> Result<String, String> {
    serde_json::to_string(&store().capabilities()).map_err(|e| e.to_string())
}

pub async fn apply(envelope_json: &str) -> Result<String, String> {
    let envelope: A2uiEnvelope =
        serde_json::from_str(envelope_json).map_err(|e| format!("invalid A2UI envelope: {e}"))?;
    let result = store().apply(envelope).await.map_err(|e| e.to_string())?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

pub async fn ingest(payload_json: &str) -> Result<String, String> {
    let value: Value =
        serde_json::from_str(payload_json).map_err(|e| format!("invalid JSON payload: {e}"))?;
    let s = store();
    s.validate_payload(&value).map_err(|e| e.to_string())?;
    let envelopes = car_a2ui::envelopes_from_value(&value).map_err(|e| e.to_string())?;
    if envelopes.is_empty() {
        return Err("no A2UI envelopes found in payload".into());
    }
    let owner = car_a2ui::owner_from_value(&value);
    let mut applied = Vec::with_capacity(envelopes.len());
    for envelope in envelopes {
        let result = s
            .apply_with_owner(envelope, owner.clone())
            .await
            .map_err(|e| e.to_string())?;
        applied.push(result);
    }
    serde_json::to_string(&serde_json::json!({ "applied": applied })).map_err(|e| e.to_string())
}

pub async fn surfaces() -> Result<String, String> {
    serde_json::to_string(&store().list().await).map_err(|e| e.to_string())
}

pub async fn get(surface_id: &str) -> Result<String, String> {
    serde_json::to_string(&store().get(surface_id).await).map_err(|e| e.to_string())
}

pub async fn reap() -> Result<String, String> {
    let removed = store().reap_expired(chrono::Utc::now()).await;
    serde_json::to_string(&serde_json::json!({ "removed": removed })).map_err(|e| e.to_string())
}

pub fn validate_payload(value_json: &str) -> Result<String, String> {
    let value: Value =
        serde_json::from_str(value_json).map_err(|e| format!("invalid JSON payload: {e}"))?;
    store()
        .validate_payload(&value)
        .map_err(|e| e.to_string())?;
    Ok("null".to_string())
}