kroki_rs/diagrams/providers/
plugin.rs1use crate::diagrams::{DiagramError, DiagramProvider, DiagramResult};
2use async_trait::async_trait;
3use std::process::Stdio;
4use tokio::process::Command;
5
6pub struct PluginProvider {
11 pub name: String,
13 pub command: String,
15 pub args: Vec<String>,
17 pub stdin: bool,
19 pub timeout_ms: u64,
21}
22
23impl PluginProvider {
24 pub fn new(config: &crate::config::PluginConfig) -> Self {
26 Self {
27 name: config.name.clone(),
28 command: config.command.clone(),
29 args: config.args.clone(),
30 stdin: config.stdin,
31 timeout_ms: config.timeout_ms.unwrap_or(5000),
32 }
33 }
34}
35
36#[async_trait]
37impl DiagramProvider for PluginProvider {
38 fn validate(&self, source: &str) -> DiagramResult<()> {
39 if source.trim().is_empty() {
40 return Err(DiagramError::ValidationFailed(
41 "Diagram source is empty".into(),
42 ));
43 }
44 Ok(())
45 }
46
47 async fn generate(&self, source: &str, format: &str) -> DiagramResult<Vec<u8>> {
48 let mut cmd = Command::new(&self.command);
49
50 for arg in &self.args {
52 cmd.arg(arg.replace("{format}", format));
53 }
54
55 if self.stdin {
56 cmd.stdin(Stdio::piped());
57 } else {
58 cmd.stdin(Stdio::null());
59 }
60
61 cmd.stdout(Stdio::piped());
62 cmd.stderr(Stdio::piped());
63
64 let input_bytes = if self.stdin {
65 Some(source.as_bytes())
66 } else {
67 None
68 };
69
70 let output = crate::diagrams::run_process_with_timeout(
71 &self.name,
72 cmd,
73 input_bytes,
74 Some(self.timeout_ms),
75 source.len(),
76 )
77 .await?;
78
79 if output.status.success() {
80 if output.stdout.is_empty() {
81 return Err(DiagramError::ProcessFailed(format!(
82 "Plugin '{}' succeeded but returned empty output",
83 self.name
84 )));
85 }
86 Ok(output.stdout)
87 } else {
88 let stderr = String::from_utf8_lossy(&output.stderr);
89 Err(DiagramError::ProcessFailed(format!(
90 "Plugin '{}' failed: {}",
91 self.name, stderr
92 )))
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[tokio::test]
102 async fn test_plugin_provider_stdout() {
103 let config = crate::config::PluginConfig {
104 name: "test-echo".to_string(),
105 command: "echo".to_string(),
106 args: vec!["hello".to_string()],
107 stdin: false,
108 formats: vec!["txt".to_string()],
109 timeout_ms: Some(1000),
110 };
111 let provider = PluginProvider::new(&config);
112 let result = provider.generate("", "txt").await.unwrap();
113 assert_eq!(String::from_utf8_lossy(&result).trim(), "hello");
114 }
115
116 #[tokio::test]
117 async fn test_plugin_provider_stdin() {
118 let config = crate::config::PluginConfig {
120 name: "test-cat".to_string(),
121 command: "cat".to_string(),
122 args: vec![],
123 stdin: true,
124 formats: vec!["txt".to_string()],
125 timeout_ms: Some(1000),
126 };
127 let provider = PluginProvider::new(&config);
128 let result = provider.generate("input-text", "txt").await.unwrap();
129 assert_eq!(String::from_utf8_lossy(&result), "input-text");
130 }
131}