1use super::design::{Description, Edge, EdgeEnding, ExternalResource, Type};
2use std::{borrow::Cow, fmt::Write};
3
4#[expect(clippy::struct_excessive_bools)]
39#[derive(Debug)]
40pub struct D2Describer {
41 pub simple_type_name: bool,
46 pub show_context_in_node: bool,
50 pub show_description: bool,
54 pub show_externals: bool,
58}
59
60impl Default for D2Describer {
61 fn default() -> Self {
62 Self {
63 simple_type_name: true,
64 show_context_in_node: false,
65 show_description: false,
66 show_externals: false,
67 }
68 }
69}
70
71fn escape_str(val: &str) -> String {
72 val.replace('<', "\\<")
73 .replace('>', "\\>")
74 .replace('{', "\\{")
75 .replace('}', "\\}")
76}
77
78impl D2Describer {
79 #[must_use]
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn modify(&mut self, func: impl FnOnce(&mut Self)) -> &mut Self {
103 func(self);
104 self
105 }
106
107 fn get_type_name<'a>(&self, r#type: &'a Type) -> Cow<'a, str> {
108 if r#type.name.is_empty() {
109 return Cow::Borrowed("\"\"");
110 }
111
112 if self.simple_type_name {
113 let res = r#type.get_name_simple();
114 if res.is_empty() {
116 return Cow::Borrowed(&r#type.name);
117 }
118 Cow::Owned(res)
119 } else {
120 Cow::Borrowed(&r#type.name)
121 }
122 }
123
124 #[must_use]
135 pub fn format(&self, desc: &Description) -> String {
136 let id = rand::random();
137 let (input, output, context) = {
138 let base = desc.get_base_ref();
139 (&base.input, &base.output, &base.context)
140 };
141 let mut res = format!(
142 r"direction: down
143classes: {{
144 node: {{
145 style.border-radius: 8
146 }}
147 flow: {{
148 style.border-radius: 8
149 }}
150 edge: {{
151 style.font-size: 18
152 }}
153 node_flow_description: {{
154 shape: page
155 }}
156 external_resource: {{
157 shape: parallelogram
158 }}
159 start_end: {{
160 shape: oval
161 style.italic: true
162 }}
163}}
164Start: {{
165 class: start_end
166 desc: |md
167 **Context**: {context}\
168 **Input**: {input}
169 |
170}}
171Start -> {id}: {input} {{
172 class: edge
173}}
174End: {{
175 class: start_end
176 desc: |md
177 **Output**: {output}
178 |
179}}
180{id} -> End: {output} {{
181 class: edge
182}}
183",
184 context = escape_str(&self.get_type_name(context)),
185 input = escape_str(&self.get_type_name(input)),
186 output = escape_str(&self.get_type_name(output)),
187 );
188
189 self.process(desc, id, &mut res);
190
191 res
192 }
193
194 fn process(&self, desc: &Description, id: u64, out: &mut String) {
195 self.start_define_base(desc, id, out);
196
197 let Description::Flow { base, nodes, edges } = desc else {
198 out.push_str("}\n");
199 return;
200 };
201
202 let nodes_and_ids = nodes
203 .iter()
204 .map(|node_desc| {
205 let id = rand::random();
206 self.process(node_desc, id, out);
207 (id, node_desc.get_base_ref())
208 })
209 .collect::<Vec<_>>();
210
211 writeln!(
212 out,
213 r"start: Start {{
214 class: start_end
215 desc: |md
216 **Context**: {context}\
217 **Input**: {input}
218 |
219 }}
220 end: End {{
221 class: start_end
222 desc: |md
223 **Output**: {output}
224 |
225 }}",
226 context = escape_str(&self.get_type_name(&base.context)),
227 input = escape_str(&self.get_type_name(&base.input)),
228 output = escape_str(&self.get_type_name(&base.output))
229 )
230 .unwrap();
231 for Edge { start, end } in edges {
232 let start_type = match start {
233 EdgeEnding::ToFlow => {
234 out.push_str("start");
235 "\"\""
236 }
237 EdgeEnding::ToNode { node_index } => {
238 let node = &nodes_and_ids[*node_index];
239 out.push_str(&node.0.to_string());
240 &escape_str(&self.get_type_name(&node.1.output))
241 }
242 };
243 out.push_str(" -> ");
244 let end_type = match end {
245 EdgeEnding::ToFlow => {
246 out.push_str("end");
247 "\"\""
248 }
249 EdgeEnding::ToNode { node_index } => {
250 let node = &nodes_and_ids[*node_index];
251 out.push_str(&node.0.to_string());
252 &escape_str(&self.get_type_name(&node.1.input))
253 }
254 };
255 writeln!(
256 out,
257 r": {{
258 class: edge
259 source-arrowhead: {start_type}
260 target-arrowhead: {end_type}
261 }}",
262 )
263 .unwrap();
264 }
265
266 out.push_str("}\n");
267 }
268
269 fn start_define_base(&self, desc: &Description, id: u64, out: &mut String) {
270 let base = desc.get_base_ref();
271 let is_node = matches!(desc, Description::Node { .. });
272 writeln!(
273 out,
274 r"{}:{} {{
275 class: {}",
276 id,
277 escape_str(&self.get_type_name(&base.r#type)),
278 if is_node { "node" } else { "flow" }
279 )
280 .unwrap();
281
282 let has_description = base.description.is_some() && self.show_description;
283 let show_context = is_node && self.show_context_in_node && !base.context.name.is_empty();
284 if has_description || show_context {
285 writeln!(out, "desc: |md").unwrap();
286 if show_context {
287 writeln!(
288 out,
289 r"**Context**: {}<br/>",
290 escape_str(&self.get_type_name(&base.context))
291 )
292 .unwrap();
293 }
294 if has_description {
295 out.push_str(&escape_str(base.description.as_ref().unwrap()));
296 }
297 writeln!(
298 out,
299 "
300 | {{
301 class: node_flow_description
302 }}",
303 )
304 .unwrap();
305 }
306
307 if !self.show_externals {
308 return;
309 }
310 let Some(externals) = &base.externals else {
311 return;
312 };
313
314 for ExternalResource {
315 r#type,
316 description,
317 output,
318 } in externals
319 {
320 let ext_id: u64 = rand::random();
321 writeln!(
322 out,
323 r"{}:{} {{
324 class: external_resource
325 desc: |md
326 **output**: {}\
327 {}
328 |
329 }}",
330 ext_id,
331 escape_str(&self.get_type_name(r#type)),
332 escape_str(&self.get_type_name(output)),
333 escape_str(description.as_ref().map(String::as_str).unwrap_or_default()),
334 )
335 .unwrap();
336 }
337 }
338}