unifier-cli 0.5.0

Filesystem postbox for inter-process communication via a Unix tree
Documentation
//! Line-delimited JSON protocol between CLI clients and the hot daemon.

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::postbox::Message;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Request {
    Put {
        key: String,
        value: String,
    },
    Get {
        key: String,
    },
    Del {
        key: String,
    },
    Send {
        #[serde(default)]
        from: Option<String>,
        recipient: String,
        message: String,
    },
    Cron {
        schedule: String,
        message: String,
    },
    Poll {
        recipient: String,
    },
    PollCron,
    List {
        path: String,
    },
    Ack {
        id_or_path: String,
    },
    Flush,
    Ping,
    Shutdown,
    TickStart {
        label: String,
    },
    TickEnd,
    TickStatus,
    TickLock {
        key: String,
    },
    TickUnlock {
        key: String,
    },
    /// Announce a named phase within the active tick (e.g. sense / decide / act).
    TickPhase {
        phase: String,
    },
    Event {
        payload: String,
        /// Seconds until the event is deleted. Omit to use the default (24h) or payload fields.
        #[serde(default)]
        ttl: Option<u64>,
    },
    AgentMessage {
        from: String,
        to: String,
        payload: String,
    },
    /// Report HTTP base URL / port for the www server.
    WebStatus,
    /// List published temp files.
    WebList,
    /// Remove a published temp file by name.
    WebRm {
        name: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum Response {
    Ok {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        value: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        uuid: Option<Uuid>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        found: Option<bool>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        dirty: Option<bool>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        tick: Option<u64>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        queued: Option<usize>,
        /// Active tick phase (`start`, custom phase, or unset when idle).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        phase: Option<String>,
        /// Latest committed tick number (tick status responses).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        committed: Option<u64>,
        /// Active tick label (tick status / start responses).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        label: Option<String>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        locked_keys: Vec<String>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        messages: Vec<MessageDto>,
    },
    Err {
        error: String,
    },
}

/// Whether a request is allowed on `.daemon/tick.sock`.
///
/// The tick socket is a capability boundary: an external tick driver may run
/// turn lifecycles without gaining the ability to mutate the board.
pub fn is_tick_socket_request(request: &Request) -> bool {
    matches!(
        request,
        Request::Ping
            | Request::TickStart { .. }
            | Request::TickEnd
            | Request::TickStatus
            | Request::TickLock { .. }
            | Request::TickUnlock { .. }
            | Request::TickPhase { .. }
    )
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageDto {
    pub id: Uuid,
    pub path: String,
    pub body: String,
}

impl From<Message> for MessageDto {
    fn from(m: Message) -> Self {
        Self {
            id: m.id,
            path: m.path.display().to_string(),
            body: m.body,
        }
    }
}

impl From<MessageDto> for Message {
    fn from(m: MessageDto) -> Self {
        Self {
            id: m.id,
            path: m.path.into(),
            body: m.body,
        }
    }
}

pub fn encode_response(response: &Response) -> Result<String, serde_json::Error> {
    Ok(format!("{}\n", serde_json::to_string(response)?))
}

pub fn decode_request(line: &str) -> Result<Request, serde_json::Error> {
    serde_json::from_str(line.trim())
}

pub fn decode_response(line: &str) -> Result<Response, serde_json::Error> {
    serde_json::from_str(line.trim())
}

pub fn ok_empty() -> Response {
    Response::Ok {
        value: None,
        uuid: None,
        found: None,
        dirty: None,
        tick: None,
        queued: None,
        phase: None,
        committed: None,
        label: None,
        locked_keys: vec![],
        messages: vec![],
    }
}