liminal-cli 0.3.0

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! `call` (SDK-017 R3): one routed request whose reply or refusal is the
//! server's own.

use serde_json::{Value, json};

use super::Rail;
use crate::mcp::tools::ToolResult;

/// How one request ended.
#[derive(Clone, PartialEq, Eq)]
pub enum Outcome {
    /// The correlated reply.
    Reply(Value),
    /// Sent without a reply asked for.
    Sent,
    /// Refused or failed, in the server's words.
    Refused(String),
}

/// The field a minted pass is carried in.
pub const PASS_FIELD: &str = "pass";

/// `input` carrying the calling seat's own pass for `audience` as `pass`
/// (SRV-029 R9). An input that already carries `pass` is refused: the pass a
/// station receives is always one the ticket minted.
///
/// # Errors
/// The words naming why no pass was put in.
pub fn with_pass<R: Rail>(rail: &R, input: &Value, audience: &str) -> Result<Value, String> {
    let Value::Object(object) = input else {
        return Err("pass_for needs an object input".to_owned());
    };
    if object.contains_key(PASS_FIELD) {
        return Err(format!(
            "the input already carries {PASS_FIELD}; with pass_for the ticket puts it in"
        ));
    }
    let pass = rail.pass_for(audience).map_err(|error| error.to_string())?;
    let mut object = object.clone();
    object.insert(PASS_FIELD.to_owned(), Value::String(pass));
    Ok(Value::Object(object))
}

/// One request on `subject`, checked against the rail's request limit
/// before anything is opened; with `pass_for`, carrying the seat's pass for
/// that audience.
pub fn request<R: Rail>(
    rail: &R,
    subject: &str,
    input: &Value,
    reply: bool,
    pass_for: Option<&str>,
) -> Outcome {
    let minted;
    let input = match pass_for {
        None => input,
        Some(audience) => match with_pass(rail, input, audience) {
            Ok(carrying) => {
                minted = carrying;
                &minted
            }
            Err(words) => return Outcome::Refused(words),
        },
    };
    let bytes = serde_json::to_vec(input).map_or(usize::MAX, |bytes| bytes.len());
    if bytes > rail.request_bytes() {
        return Outcome::Refused(format!(
            "the request is over {} bytes and was not sent",
            rail.request_bytes()
        ));
    }
    match rail.request(subject, input, reply) {
        Ok(Some(answer)) => Outcome::Reply(answer),
        Ok(None) => Outcome::Sent,
        Err(error) => Outcome::Refused(error.to_string()),
    }
}

/// The `call` tool.
pub fn call<R: Rail>(
    rail: &R,
    subject: &str,
    input: &Value,
    reply: bool,
    pass_for: Option<&str>,
) -> ToolResult {
    match request(rail, subject, input, reply, pass_for) {
        Outcome::Reply(answer) => ToolResult::ok(answer),
        Outcome::Sent => ToolResult::ok(json!({ "subject": subject, "sent": true })),
        Outcome::Refused(words) => ToolResult::failed(&words, json!({ "subject": subject })),
    }
}

#[cfg(test)]
#[path = "call_tests.rs"]
mod tests;

impl std::fmt::Debug for Outcome {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Reply(_) => "Reply",
            Self::Sent => "Sent",
            Self::Refused(_) => "Refused",
        })
    }
}