Skip to main content

liminal_cli/network/
call.rs

1//! `call` (SDK-017 R3): one routed request whose reply or refusal is the
2//! server's own.
3
4use serde_json::{Value, json};
5
6use super::Rail;
7use crate::mcp::tools::ToolResult;
8
9/// How one request ended.
10#[derive(Clone, PartialEq, Eq)]
11pub enum Outcome {
12    /// The correlated reply.
13    Reply(Value),
14    /// Sent without a reply asked for.
15    Sent,
16    /// Refused or failed, in the server's words.
17    Refused(String),
18}
19
20/// The field a minted pass is carried in.
21pub const PASS_FIELD: &str = "pass";
22
23/// `input` carrying the calling seat's own pass for `audience` as `pass`
24/// (SRV-029 R9). An input that already carries `pass` is refused: the pass a
25/// station receives is always one the ticket minted.
26///
27/// # Errors
28/// The words naming why no pass was put in.
29pub fn with_pass<R: Rail>(rail: &R, input: &Value, audience: &str) -> Result<Value, String> {
30    let Value::Object(object) = input else {
31        return Err("pass_for needs an object input".to_owned());
32    };
33    if object.contains_key(PASS_FIELD) {
34        return Err(format!(
35            "the input already carries {PASS_FIELD}; with pass_for the ticket puts it in"
36        ));
37    }
38    let pass = rail.pass_for(audience).map_err(|error| error.to_string())?;
39    let mut object = object.clone();
40    object.insert(PASS_FIELD.to_owned(), Value::String(pass));
41    Ok(Value::Object(object))
42}
43
44/// One request on `subject`, checked against the rail's request limit
45/// before anything is opened; with `pass_for`, carrying the seat's pass for
46/// that audience.
47pub fn request<R: Rail>(
48    rail: &R,
49    subject: &str,
50    input: &Value,
51    reply: bool,
52    pass_for: Option<&str>,
53) -> Outcome {
54    let minted;
55    let input = match pass_for {
56        None => input,
57        Some(audience) => match with_pass(rail, input, audience) {
58            Ok(carrying) => {
59                minted = carrying;
60                &minted
61            }
62            Err(words) => return Outcome::Refused(words),
63        },
64    };
65    let bytes = serde_json::to_vec(input).map_or(usize::MAX, |bytes| bytes.len());
66    if bytes > rail.request_bytes() {
67        return Outcome::Refused(format!(
68            "the request is over {} bytes and was not sent",
69            rail.request_bytes()
70        ));
71    }
72    match rail.request(subject, input, reply) {
73        Ok(Some(answer)) => Outcome::Reply(answer),
74        Ok(None) => Outcome::Sent,
75        Err(error) => Outcome::Refused(error.to_string()),
76    }
77}
78
79/// The `call` tool.
80pub fn call<R: Rail>(
81    rail: &R,
82    subject: &str,
83    input: &Value,
84    reply: bool,
85    pass_for: Option<&str>,
86) -> ToolResult {
87    match request(rail, subject, input, reply, pass_for) {
88        Outcome::Reply(answer) => ToolResult::ok(answer),
89        Outcome::Sent => ToolResult::ok(json!({ "subject": subject, "sent": true })),
90        Outcome::Refused(words) => ToolResult::failed(&words, json!({ "subject": subject })),
91    }
92}
93
94#[cfg(test)]
95#[path = "call_tests.rs"]
96mod tests;
97
98impl std::fmt::Debug for Outcome {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.write_str(match self {
101            Self::Reply(_) => "Reply",
102            Self::Sent => "Sent",
103            Self::Refused(_) => "Refused",
104        })
105    }
106}