Skip to main content

sen_plugin_api/
lib.rs

1//! sen-plugin-api: Shared types for sen-rs plugin system
2//!
3//! This crate defines the protocol between host and guest (wasm plugin).
4//! Communication uses MessagePack serialization.
5
6use serde::{Deserialize, Serialize};
7
8/// API version for compatibility checking
9pub const API_VERSION: u32 = 1;
10
11/// Command specification returned by plugin's `manifest()` function
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CommandSpec {
14    /// Command name (used for routing, e.g., "hello" or "db:create")
15    pub name: String,
16
17    /// Short description for help text
18    pub about: String,
19
20    /// Plugin version (semver)
21    #[serde(default)]
22    pub version: Option<String>,
23
24    /// Plugin author
25    #[serde(default)]
26    pub author: Option<String>,
27
28    /// Argument specifications
29    #[serde(default)]
30    pub args: Vec<ArgSpec>,
31
32    /// Nested subcommands
33    #[serde(default)]
34    pub subcommands: Vec<CommandSpec>,
35}
36
37/// Argument specification
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ArgSpec {
40    /// Argument name (positional) or option name
41    pub name: String,
42
43    /// Long option name (e.g., "--output")
44    #[serde(default)]
45    pub long: Option<String>,
46
47    /// Short option name (e.g., "-o")
48    #[serde(default)]
49    pub short: Option<char>,
50
51    /// Whether this argument is required
52    #[serde(default)]
53    pub required: bool,
54
55    /// Help text for this argument
56    #[serde(default)]
57    pub help: String,
58
59    /// Value placeholder name (e.g., "FILE")
60    #[serde(default)]
61    pub value_name: Option<String>,
62
63    /// Default value if not provided
64    #[serde(default)]
65    pub default_value: Option<String>,
66
67    /// List of allowed values
68    #[serde(default)]
69    pub possible_values: Option<Vec<String>>,
70}
71
72/// Result of plugin execution
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub enum ExecuteResult {
75    /// Successful execution with output
76    Success(String),
77
78    /// Execution failed
79    Error(ExecuteError),
80}
81
82/// Error details from plugin execution
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ExecuteError {
85    /// Exit code (1 = user error, 101 = system error)
86    pub code: u8,
87
88    /// Error message
89    pub message: String,
90}
91
92/// Plugin manifest with API version
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct PluginManifest {
95    /// API version for compatibility
96    pub api_version: u32,
97
98    /// Command specification
99    pub command: CommandSpec,
100}
101
102impl PluginManifest {
103    /// Create a new plugin manifest with current API version
104    pub fn new(command: CommandSpec) -> Self {
105        Self {
106            api_version: API_VERSION,
107            command,
108        }
109    }
110}
111
112impl ExecuteResult {
113    /// Create a success result
114    pub fn success(output: impl Into<String>) -> Self {
115        Self::Success(output.into())
116    }
117
118    /// Create a user error (exit code 1)
119    pub fn user_error(message: impl Into<String>) -> Self {
120        Self::Error(ExecuteError {
121            code: 1,
122            message: message.into(),
123        })
124    }
125
126    /// Create a system error (exit code 101)
127    pub fn system_error(message: impl Into<String>) -> Self {
128        Self::Error(ExecuteError {
129            code: 101,
130            message: message.into(),
131        })
132    }
133}
134
135impl CommandSpec {
136    /// Create a new command spec
137    pub fn new(name: impl Into<String>, about: impl Into<String>) -> Self {
138        Self {
139            name: name.into(),
140            about: about.into(),
141            version: None,
142            author: None,
143            args: Vec::new(),
144            subcommands: Vec::new(),
145        }
146    }
147
148    /// Add version
149    pub fn version(mut self, version: impl Into<String>) -> Self {
150        self.version = Some(version.into());
151        self
152    }
153
154    /// Add an argument
155    pub fn arg(mut self, arg: ArgSpec) -> Self {
156        self.args.push(arg);
157        self
158    }
159
160    /// Add a subcommand
161    pub fn subcommand(mut self, cmd: CommandSpec) -> Self {
162        self.subcommands.push(cmd);
163        self
164    }
165}
166
167impl ArgSpec {
168    /// Create a positional argument
169    pub fn positional(name: impl Into<String>) -> Self {
170        Self {
171            name: name.into(),
172            long: None,
173            short: None,
174            required: false,
175            help: String::new(),
176            value_name: None,
177            default_value: None,
178            possible_values: None,
179        }
180    }
181
182    /// Create an option with long name
183    pub fn option(name: impl Into<String>, long: impl Into<String>) -> Self {
184        Self {
185            name: name.into(),
186            long: Some(long.into()),
187            short: None,
188            required: false,
189            help: String::new(),
190            value_name: None,
191            default_value: None,
192            possible_values: None,
193        }
194    }
195
196    /// Set as required
197    pub fn required(mut self) -> Self {
198        self.required = true;
199        self
200    }
201
202    /// Set help text
203    pub fn help(mut self, help: impl Into<String>) -> Self {
204        self.help = help.into();
205        self
206    }
207
208    /// Set short option
209    pub fn short(mut self, short: char) -> Self {
210        self.short = Some(short);
211        self
212    }
213
214    /// Set default value
215    pub fn default(mut self, value: impl Into<String>) -> Self {
216        self.default_value = Some(value.into());
217        self
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_command_spec_serialization() {
227        let spec = CommandSpec::new("hello", "Says hello")
228            .version("1.0.0")
229            .arg(
230                ArgSpec::positional("name")
231                    .help("Name to greet")
232                    .default("World"),
233            );
234
235        let bytes = rmp_serde::to_vec(&spec).unwrap();
236        let decoded: CommandSpec = rmp_serde::from_slice(&bytes).unwrap();
237
238        assert_eq!(decoded.name, "hello");
239        assert_eq!(decoded.about, "Says hello");
240        assert_eq!(decoded.args.len(), 1);
241    }
242
243    #[test]
244    fn test_execute_result_serialization() {
245        let result = ExecuteResult::success("Hello, World!");
246        let bytes = rmp_serde::to_vec(&result).unwrap();
247        let decoded: ExecuteResult = rmp_serde::from_slice(&bytes).unwrap();
248
249        match decoded {
250            ExecuteResult::Success(s) => assert_eq!(s, "Hello, World!"),
251            _ => panic!("Expected success"),
252        }
253    }
254}