Skip to main content

merman_bindings_core/
engine.rs

1use crate::{BindingError, common};
2
3#[derive(Clone)]
4pub struct BindingEngine {
5    #[cfg(feature = "render")]
6    render: crate::render::CachedRenderEngine,
7    #[cfg(feature = "ascii")]
8    ascii: crate::ascii::CachedAsciiEngine,
9}
10
11impl BindingEngine {
12    pub fn new(options_json: &[u8]) -> Result<Self, BindingError> {
13        #[cfg(any(feature = "render", feature = "ascii"))]
14        {
15            let options = common::parse_options(options_json)?;
16            return Ok(Self {
17                #[cfg(feature = "render")]
18                render: crate::render::CachedRenderEngine::new(&options)?,
19                #[cfg(feature = "ascii")]
20                ascii: crate::ascii::CachedAsciiEngine::new(&options)?,
21            });
22        }
23
24        #[cfg(not(any(feature = "render", feature = "ascii")))]
25        {
26            let _ = options_json;
27            Ok(Self {})
28        }
29    }
30
31    pub fn render_svg(&self, source: &[u8]) -> Result<Vec<u8>, BindingError> {
32        #[cfg(feature = "render")]
33        {
34            self.render.render_svg(source)
35        }
36
37        #[cfg(not(feature = "render"))]
38        {
39            let _ = source;
40            Err(common::feature_required_error("SVG rendering", "render"))
41        }
42    }
43
44    pub fn render_ascii(&self, source: &[u8]) -> Result<Vec<u8>, BindingError> {
45        #[cfg(feature = "ascii")]
46        {
47            self.ascii.render_ascii(source)
48        }
49
50        #[cfg(not(feature = "ascii"))]
51        {
52            let _ = source;
53            Err(common::feature_required_error("ASCII rendering", "ascii"))
54        }
55    }
56
57    pub fn parse_json(&self, source: &[u8]) -> Result<Vec<u8>, BindingError> {
58        #[cfg(feature = "render")]
59        {
60            self.render.parse_json(source)
61        }
62
63        #[cfg(not(feature = "render"))]
64        {
65            let _ = source;
66            Err(common::feature_required_error("parse_json", "render"))
67        }
68    }
69
70    pub fn layout_json(&self, source: &[u8]) -> Result<Vec<u8>, BindingError> {
71        #[cfg(feature = "render")]
72        {
73            self.render.layout_json(source)
74        }
75
76        #[cfg(not(feature = "render"))]
77        {
78            let _ = source;
79            Err(common::feature_required_error("layout_json", "render"))
80        }
81    }
82
83    pub fn validate_json(&self, source: &[u8]) -> Result<Vec<u8>, BindingError> {
84        #[cfg(feature = "render")]
85        {
86            self.render.validate_json(source)
87        }
88
89        #[cfg(not(feature = "render"))]
90        {
91            let _ = source;
92            common::validation_payload_json(Err(common::feature_required_error(
93                "validation",
94                "render",
95            )))
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use serde_json::Value;
104    use std::sync::Arc;
105
106    #[test]
107    fn engine_reuses_options_for_rendering() {
108        let engine = BindingEngine::new(
109            br#"{
110                "layout": { "text_measurer": "deterministic" },
111                "svg": { "diagram_id": "cached engine", "pipeline": "readable" }
112            }"#,
113        )
114        .unwrap();
115
116        let svg = engine.render_svg(b"flowchart TD\nA[Hello]");
117        if cfg!(feature = "render") {
118            let svg = String::from_utf8(svg.unwrap()).unwrap();
119            assert!(svg.contains("id=\"cached-engine\""));
120            assert!(svg.contains("data-merman-foreignobject"));
121        } else {
122            assert_eq!(
123                svg.unwrap_err().status(),
124                crate::BindingStatus::UnsupportedFormat
125            );
126        }
127    }
128
129    #[test]
130    fn engine_validates_with_cached_renderer() {
131        let engine = BindingEngine::new(b"").unwrap();
132        let validation: Value =
133            serde_json::from_slice(&engine.validate_json(b"").unwrap()).unwrap();
134
135        if cfg!(feature = "render") {
136            assert_eq!(validation["valid"], false);
137            assert_eq!(validation["code_name"], "MERMAN_NO_DIAGRAM");
138        } else {
139            assert_eq!(validation["valid"], false);
140            assert_eq!(validation["code_name"], "MERMAN_UNSUPPORTED_FORMAT");
141        }
142    }
143
144    #[test]
145    fn engine_can_render_concurrently() {
146        let engine = Arc::new(BindingEngine::new(b"").unwrap());
147        let mut handles = Vec::new();
148
149        for _ in 0..8 {
150            let engine = Arc::clone(&engine);
151            handles.push(std::thread::spawn(move || {
152                for _ in 0..8 {
153                    let svg = engine.render_svg(b"flowchart TD\nA[Hello] --> B[World]");
154                    if cfg!(feature = "render") {
155                        let svg = String::from_utf8(svg.unwrap()).unwrap();
156                        assert!(svg.contains("<svg"));
157                    } else {
158                        let err = svg.unwrap_err();
159                        assert_eq!(err.status(), crate::BindingStatus::UnsupportedFormat);
160                    }
161                }
162            }));
163        }
164
165        for handle in handles {
166            handle.join().unwrap();
167        }
168    }
169}