merman_bindings_core/
engine.rs1use 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}