Skip to main content

mermkit/
lib.rs

1use base64::engine::general_purpose::STANDARD;
2use base64::Engine;
3use serde::Deserialize;
4use std::env;
5use std::io::{BufRead, BufReader, Write};
6use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
7
8#[derive(Debug)]
9pub struct RenderResult {
10    pub bytes: Vec<u8>,
11    pub mime: String,
12    pub warnings: Vec<String>,
13}
14
15#[derive(Debug, Deserialize)]
16struct RenderPayload {
17    bytes: Option<String>,
18    mime: Option<String>,
19    warnings: Option<Vec<String>>,
20}
21
22#[derive(Debug, Deserialize)]
23struct ServeResponse {
24    ok: bool,
25    result: Option<RenderPayload>,
26    error: Option<String>,
27}
28
29pub struct Client {
30    child: Child,
31    stdin: ChildStdin,
32    stdout: BufReader<ChildStdout>,
33}
34
35impl Client {
36    pub fn new() -> Result<Self, String> {
37        let mut child = Command::new(get_binary())
38            .arg("serve")
39            .stdin(Stdio::piped())
40            .stdout(Stdio::piped())
41            .spawn()
42            .map_err(|e| e.to_string())?;
43
44        let stdin = child.stdin.take().ok_or_else(|| "failed to open stdin".to_string())?;
45        let stdout = child.stdout.take().ok_or_else(|| "failed to open stdout".to_string())?;
46        Ok(Self {
47            child,
48            stdin,
49            stdout: BufReader::new(stdout),
50        })
51    }
52
53    pub fn render(
54        &mut self,
55        source: &str,
56        format: &str,
57        theme: Option<&str>,
58        engine: Option<&str>,
59    ) -> Result<RenderResult, String> {
60        let mut options = serde_json::Map::new();
61        options.insert("format".to_string(), serde_json::Value::String(format.to_string()));
62        if let Some(t) = theme {
63            options.insert("theme".to_string(), serde_json::Value::String(t.to_string()));
64        }
65        if let Some(e) = engine {
66            options.insert("engine".to_string(), serde_json::Value::String(e.to_string()));
67        }
68
69        let request = serde_json::json!({
70            "action": "render",
71            "diagram": source,
72            "options": options
73        });
74
75        let line = serde_json::to_string(&request).map_err(|e| e.to_string())?;
76        self.stdin.write_all(line.as_bytes()).map_err(|e| e.to_string())?;
77        self.stdin.write_all(b"\n").map_err(|e| e.to_string())?;
78        self.stdin.flush().map_err(|e| e.to_string())?;
79
80        let mut response_line = String::new();
81        self.stdout.read_line(&mut response_line).map_err(|e| e.to_string())?;
82        let resp: ServeResponse = serde_json::from_str(&response_line).map_err(|e| e.to_string())?;
83        if !resp.ok {
84            return Err(resp.error.unwrap_or_else(|| "mermkit render failed".to_string()));
85        }
86
87        let payload = resp.result.ok_or_else(|| "missing result".to_string())?;
88        let bytes_b64 = payload.bytes.ok_or_else(|| "mermkit render returned no bytes".to_string())?;
89        let bytes = STANDARD.decode(bytes_b64).map_err(|e| e.to_string())?;
90
91        Ok(RenderResult {
92            bytes,
93            mime: payload.mime.unwrap_or_else(|| "application/octet-stream".to_string()),
94            warnings: payload.warnings.unwrap_or_default(),
95        })
96    }
97}
98
99pub fn render(source: &str, format: &str, theme: Option<&str>, engine: Option<&str>) -> Result<RenderResult, String> {
100    let mut args = vec!["render", "--stdin", "--format", format, "--json"];
101    if let Some(t) = theme {
102        args.push("--theme");
103        args.push(t);
104    }
105    if let Some(e) = engine {
106        args.push("--engine");
107        args.push(e);
108    }
109
110    let mut child = Command::new(get_binary())
111        .args(args)
112        .stdin(Stdio::piped())
113        .stdout(Stdio::piped())
114        .stderr(Stdio::piped())
115        .spawn()
116        .map_err(|e| e.to_string())?;
117
118    if let Some(stdin) = child.stdin.as_mut() {
119        use std::io::Write;
120        stdin.write_all(source.as_bytes()).map_err(|e| e.to_string())?;
121    }
122
123    let output = child.wait_with_output().map_err(|e| e.to_string())?;
124    if !output.status.success() {
125        let err = String::from_utf8_lossy(&output.stderr);
126        return Err(err.trim().to_string());
127    }
128
129    let payload: RenderPayload = serde_json::from_slice(&output.stdout).map_err(|e| e.to_string())?;
130    let bytes_b64 = payload.bytes.ok_or_else(|| "mermkit render returned no bytes".to_string())?;
131    let bytes = STANDARD.decode(bytes_b64).map_err(|e| e.to_string())?;
132
133    Ok(RenderResult {
134        bytes,
135        mime: payload.mime.unwrap_or_else(|| "application/octet-stream".to_string()),
136        warnings: payload.warnings.unwrap_or_default(),
137    })
138}
139
140fn get_binary() -> String {
141    env::var("MERMKIT_BIN").unwrap_or_else(|_| "mermkit".to_string())
142}