Skip to main content

statespace_tool_runtime/
protocol.rs

1//! Tool execution request/response protocol.
2
3use crate::validate_env_map;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct ActionRequest {
10    pub command: Vec<String>,
11    #[serde(default)]
12    pub env: HashMap<String, String>,
13}
14
15impl ActionRequest {
16    /// # Errors
17    ///
18    /// Returns an error when the command is empty.
19    pub fn validate(&self) -> Result<(), String> {
20        if self.command.is_empty() {
21            return Err("Command cannot be empty".to_string());
22        }
23        validate_env_map(&self.env).map_err(|e| e.to_string())?;
24        Ok(())
25    }
26}
27
28#[derive(Debug, Serialize)]
29pub struct ActionResponse {
30    pub stdout: String,
31    pub stderr: String,
32    pub returncode: i32,
33}
34
35impl ActionResponse {
36    #[must_use]
37    pub const fn success(output: String) -> Self {
38        Self {
39            stdout: output,
40            stderr: String::new(),
41            returncode: 0,
42        }
43    }
44
45    #[must_use]
46    pub const fn error(message: String) -> Self {
47        Self {
48            stdout: String::new(),
49            stderr: message,
50            returncode: 1,
51        }
52    }
53}
54
55/// Envelope for successful POST responses.
56#[derive(Debug, Serialize)]
57pub struct SuccessResponse {
58    pub data: ActionResponse,
59}
60
61impl SuccessResponse {
62    #[must_use]
63    pub const fn ok(data: ActionResponse) -> Self {
64        Self { data }
65    }
66}
67
68/// Standard JSON error response.
69#[derive(Debug, Serialize)]
70pub struct ErrorResponse {
71    pub error: String,
72}
73
74impl ErrorResponse {
75    #[must_use]
76    pub fn new(message: impl Into<String>) -> Self {
77        Self {
78            error: format!("{}. See / for API instructions.", message.into()),
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_action_request_validation() {
89        let valid = ActionRequest {
90            command: vec!["ls".to_string()],
91
92            env: HashMap::new(),
93        };
94        assert!(valid.validate().is_ok());
95
96        let invalid_command = ActionRequest {
97            command: vec![],
98
99            env: HashMap::new(),
100        };
101        assert!(invalid_command.validate().is_err());
102
103        let invalid_env = ActionRequest {
104            command: vec!["ls".to_string()],
105
106            env: HashMap::from([("USER-ID".to_string(), "42".to_string())]),
107        };
108        assert!(invalid_env.validate().is_err());
109    }
110
111    #[test]
112    fn test_action_response() {
113        let success = ActionResponse::success("file1.md\nfile2.md".to_string());
114        assert_eq!(success.returncode, 0);
115        assert_eq!(success.stdout, "file1.md\nfile2.md");
116        assert_eq!(success.stderr, "");
117
118        let error = ActionResponse::error("command not found".to_string());
119        assert_eq!(error.returncode, 1);
120        assert_eq!(error.stdout, "");
121        assert_eq!(error.stderr, "command not found");
122    }
123
124    #[test]
125    fn test_error_response_points_to_root_api_guide() {
126        let error = ErrorResponse::new("File not found");
127
128        assert_eq!(error.error, "File not found. See / for API instructions.");
129    }
130}