1use crate::draw::{filter::DiagramContentFilter, OutputFormat, UML_PROGRAM};
7use crate::exec::{exec_with_temp_input, CommandArg};
8use sdml_core::error::Error;
9use sdml_core::model::annotations::AnnotationProperty;
10use sdml_core::model::definitions::{
11 DatatypeDef, EntityDef, EnumDef, EventDef, PropertyDef, RdfDef, StructureDef, TypeVariant,
12 UnionDef, ValueVariant,
13};
14use sdml_core::model::identifiers::{Identifier, IdentifierReference};
15use sdml_core::model::members::{
16 Cardinality, Member, MemberDef, MemberKind, Ordering, TypeReference, Uniqueness,
17 DEFAULT_CARDINALITY,
18};
19use sdml_core::model::modules::Module;
20use sdml_core::model::walk::{walk_module_simple, SimpleModuleVisitor};
21use sdml_core::model::{HasName, HasNameReference, HasOptionalBody, References};
22use sdml_core::store::ModuleStore;
23use sdml_core::syntax::{KW_ORDERING_ORDERED, KW_UNIQUENESS_UNIQUE};
24use std::collections::HashSet;
25use std::io::Write;
26use std::path::{Path, PathBuf};
27use tracing::{debug, trace};
28
29use super::filter::DefinitionKind;
30
31#[derive(Clone, Debug, Default)]
36pub struct UmlDiagramGenerator {
37 buffer: String,
38 imports: (String, String),
39 output: Option<DiagramOutput>,
40 assoc_src: Option<String>,
41 refs: Option<String>,
42 options: UmlDiagramOptions,
43}
44
45#[derive(Clone, Debug, Default)]
46pub struct UmlDiagramOptions {
47 emit_annotations: bool,
48 content_filter: DiagramContentFilter,
49 output_format: OutputFormat,
50}
51
52#[derive(Clone, Debug, Default)]
57struct DiagramOutput {
58 file_name: String,
59 output_dir: String,
60}
61
62impl UmlDiagramOptions {
67 pub fn with_content_filter(self, content_filter: DiagramContentFilter) -> Self {
68 Self {
69 content_filter,
70 ..self
71 }
72 }
73
74 pub fn with_output_format(self, output_format: OutputFormat) -> Self {
75 Self {
76 output_format,
77 ..self
78 }
79 }
80
81 pub fn emit_annotations(self, emit_annotations: bool) -> Self {
82 Self {
83 emit_annotations,
84 ..self
85 }
86 }
87}
88
89impl crate::Generator for UmlDiagramGenerator {
92 type Options = UmlDiagramOptions;
93
94 fn generate_with_options<W>(
95 &mut self,
96 module: &Module,
97 _: &impl ModuleStore,
98 options: Self::Options,
99 _: Option<PathBuf>,
100 writer: &mut W,
101 ) -> Result<(), Error>
102 where
103 W: Write + Sized,
104 {
105 trace_entry!(
106 "UmlDiagramGenerator",
107 "write_to_file_in_format" =>
108 "{}, _",
109 module.name());
110
111 self.options = options;
112 self.imports = make_imports(module);
113
114 walk_module_simple(module, self, true, true)?;
115
116 if self.options.output_format == OutputFormat::Source {
117 writer.write_all(self.buffer.as_bytes())?;
118 } else {
119 match exec_with_temp_input(
120 UML_PROGRAM,
121 vec![
123 CommandArg::new(format!(
124 "-o{}",
125 self.output.as_ref().map(|o| &o.output_dir).unwrap()
126 )),
127 format_to_arg(self.options.output_format),
128 ],
129 &self.buffer,
130 ) {
131 Ok(result) => {
132 debug!("Response from command: {:?}", result);
133 }
134 Err(e) => {
135 panic!("exec_with_input failed: {:?}", e);
136 }
137 }
138 }
139
140 Ok(())
141 }
142}
143
144impl SimpleModuleVisitor for UmlDiagramGenerator {
145 fn module_start(&mut self, module: &Module) -> Result<bool, Error> {
146 trace!("start_module");
147
148 let name = module.name();
149 self.buffer.push_str(&format!(
150 r#"@startuml {}
151skinparam backgroundColor transparent
152skinparam style strictuml
153skinparam linetype polyline
154skinparam nodesep 50
155
156hide methods
157hide circle
158
159show << datatype >> circle
160show << entity >> circle
161show enum circle
162show << event >> circle
163show << union >> circle
164
165{}
166package "{name}" as {} <<module>> {{
167"#,
168 self.output
169 .as_ref()
170 .map(|o| o.file_name.to_string())
171 .unwrap_or_else(|| name.to_string()),
172 self.imports.0,
173 make_id(name),
174 ));
175
176 Self::INCLUDE_NESTED
177 }
178
179 fn module_end(&mut self, _: &Module) -> Result<(), Error> {
180 if let Some(refs) = &self.refs {
181 self.buffer.push_str(refs);
182 }
183 self.buffer.push_str(&format!(
184 r#"}}
185
186{}
187
188@enduml
189"#,
190 &self.imports.1
191 ));
192
193 Ok(())
194 }
195
196 fn annotation_property(&mut self, property: &AnnotationProperty) -> Result<(), Error> {
197 if self.options.emit_annotations {
198 let name = property.name_reference();
199 let value = property.value();
200 self.buffer.push_str(&format!("{{{name} = {value}}}\n"));
201 }
202 Ok(())
203 }
204
205 fn datatype_start(&mut self, datatype: &DatatypeDef) -> Result<bool, Error> {
206 let name = datatype.name();
207 if self
208 .options
209 .content_filter
210 .draw_definition_named(DefinitionKind::Datatype, name)
211 {
212 self.buffer
213 .push_str(&start_type_with_sterotype("class", name, "datatype"));
214
215 let base_type = datatype.base_type();
218 let restriction = format!(" {} --|> {}\n", make_id(name), make_id(base_type));
219 self.refs = Some(
220 self.refs
221 .clone()
222 .map(|r| format!("{r}{restriction}"))
223 .unwrap_or(restriction),
224 );
225 self.options.emit_annotations = true;
226 }
227 Self::INCLUDE_NESTED
228 }
229
230 fn datatype_end(&mut self, datatype: &DatatypeDef) -> Result<(), Error> {
231 let name = datatype.name();
232 self.buffer.push_str(" }\n");
233 self.buffer.push_str(&format!(
234 " hide {} {}\n",
235 make_id(name),
236 if datatype.has_body() {
237 "methods"
238 } else {
239 "members"
240 }
241 ));
242
243 self.assoc_src = None;
244 self.options.emit_annotations = false;
245
246 Ok(())
247 }
248
249 fn entity_start(&mut self, entity: &EntityDef) -> Result<bool, Error> {
250 let name = entity.name();
251 if self
252 .options
253 .content_filter
254 .draw_definition_named(DefinitionKind::Entity, name)
255 {
256 self.buffer.push_str(&start_type_with_sterotype(
257 if entity.has_body() {
258 "class"
259 } else {
260 "abstract"
261 },
262 name,
263 "entity",
264 ));
265 self.assoc_src = Some(name.to_string());
266 }
267 Self::INCLUDE_NESTED
268 }
269
270 fn entity_end(&mut self, entity: &EntityDef) -> Result<(), Error> {
271 self.buffer
272 .push_str(&end_type(entity.name(), entity.has_body()));
273 self.assoc_src = None;
274 Ok(())
275 }
276
277 fn enum_start(&mut self, an_enum: &EnumDef) -> Result<bool, Error> {
278 let name = an_enum.name();
279 if self
280 .options
281 .content_filter
282 .draw_definition_named(DefinitionKind::Enum, name)
283 {
284 self.buffer
285 .push_str(&start_type_with_sterotype("class", an_enum.name(), "enum"));
286 }
287 Self::INCLUDE_NESTED
288 }
289
290 fn enum_end(&mut self, an_enum: &EnumDef) -> Result<(), Error> {
291 self.buffer
292 .push_str(&end_type(an_enum.name(), an_enum.has_body()));
293 self.assoc_src = None;
294 Ok(())
295 }
296
297 fn event_start(&mut self, event: &EventDef) -> Result<bool, Error> {
298 let name = event.name();
299 if self
300 .options
301 .content_filter
302 .draw_definition_named(DefinitionKind::Enum, name)
303 {
304 let source = event.event_source();
305 self.buffer
306 .push_str(&start_type_with_sterotype("class", name, "event"));
307 self.assoc_src = Some(name.to_string());
308 let reference = format!(" {} ..> {}: <<source>>\n", make_id(name), make_id(source));
309 self.refs = Some(
310 self.refs
311 .clone()
312 .map(|r| format!("{r}{reference}"))
313 .unwrap_or(reference),
314 );
315 }
316 Self::INCLUDE_NESTED
317 }
318
319 fn event_end(&mut self, event: &EventDef) -> Result<(), Error> {
320 self.buffer
321 .push_str(&end_type(event.name(), event.has_body()));
322 self.assoc_src = None;
323 Ok(())
324 }
325
326 fn property_start(&mut self, property: &PropertyDef) -> Result<bool, Error> {
327 let defn = property.member_def();
328 if self
329 .options
330 .content_filter
331 .draw_definition_named(DefinitionKind::Enum, defn.name())
332 {
333 self.buffer
334 .push_str(&start_type_with_sterotype("class", defn.name(), "property"));
335 }
336 Self::INCLUDE_NESTED
337 }
338
339 fn property_end(&mut self, property: &PropertyDef) -> Result<(), Error> {
340 let defn = property.member_def();
341 self.buffer
342 .push_str(&end_type(defn.name(), defn.has_body()));
343 Ok(())
344 }
345
346 fn structure_start(&mut self, structure: &StructureDef) -> Result<bool, Error> {
347 let name = structure.name();
348 if self
349 .options
350 .content_filter
351 .draw_definition_named(DefinitionKind::Enum, name)
352 {
353 self.buffer
354 .push_str(&start_type_with_sterotype("class", name, "structure"));
355 self.assoc_src = Some(name.to_string());
356 }
357 Self::INCLUDE_NESTED
358 }
359
360 fn structure_end(&mut self, structure: &StructureDef) -> Result<(), Error> {
361 self.buffer
362 .push_str(&end_type(structure.name(), structure.has_body()));
363 self.assoc_src = None;
364 Ok(())
365 }
366
367 fn rdf_start(&mut self, rdf: &RdfDef) -> Result<bool, Error> {
368 let name = rdf.name();
369 if self
370 .options
371 .content_filter
372 .draw_definition_named(DefinitionKind::Enum, name)
373 {
374 self.buffer
375 .push_str(&start_type_with_sterotype("class", name, "rdf"));
376 self.assoc_src = Some(name.to_string());
377 }
378 Self::INCLUDE_NESTED
379 }
380
381 fn rdf_end(&mut self, rdf: &RdfDef) -> Result<(), Error> {
382 self.buffer.push_str(&end_type(rdf.name(), false));
383 self.assoc_src = None;
384 Ok(())
385 }
386
387 fn union_start(&mut self, union: &UnionDef) -> Result<bool, Error> {
388 let name = union.name();
389 if self
390 .options
391 .content_filter
392 .draw_definition_named(DefinitionKind::Enum, name)
393 {
394 self.buffer
395 .push_str(&start_type_with_sterotype("class", name, "union"));
396 self.assoc_src = Some(name.to_string());
397 }
398 Self::INCLUDE_NESTED
399 }
400
401 fn union_end(&mut self, union: &UnionDef) -> Result<(), Error> {
402 self.buffer
403 .push_str(&end_type(union.name(), union.has_body()));
404 self.assoc_src = None;
405 Ok(())
406 }
407
408 fn member_start(&mut self, member: &Member) -> Result<bool, Error> {
409 match self.make_member(self.assoc_src.as_ref().unwrap(), member) {
410 (v, false) => {
411 self.refs = Some(self.refs.clone().map(|r| format!("{r}{v}")).unwrap_or(v))
412 }
413 (v, true) => self.buffer.push_str(v.as_str()),
414 }
415 Self::INCLUDE_NESTED
416 }
417
418 fn identity_member_start(&mut self, _thing: &Member) -> Result<bool, Error> {
419 Self::INCLUDE_NESTED
420 }
421
422 fn value_variant_start(&mut self, variant: &ValueVariant) -> Result<bool, Error> {
423 self.buffer.push_str(&format!(" +{}\n", variant.name()));
424 Self::INCLUDE_NESTED
425 }
426
427 fn type_variant_start(&mut self, variant: &TypeVariant) -> Result<bool, Error> {
428 let name = variant.name();
429 let rename = variant.rename();
430 let reference = if let Some(rename) = rename {
431 format!(
432 " {} *--> \"{rename}\" {}\n",
433 make_id(self.assoc_src.as_ref().unwrap()),
434 make_id(name),
435 )
436 } else {
437 format!(
438 " {} *--> {}\n",
439 make_id(self.assoc_src.as_ref().unwrap()),
440 make_id(name),
441 )
442 };
443 self.refs = Some(
444 self.refs
445 .clone()
446 .map(|r| format!("{r}{reference}"))
447 .unwrap_or(reference),
448 );
449 Self::INCLUDE_NESTED
450 }
451}
452
453impl UmlDiagramGenerator {
454 fn make_member(&self, source: &str, member: &Member) -> (String, bool) {
455 match &member.kind() {
456 MemberKind::Reference(v) => self.make_property_ref(v),
457 MemberKind::Definition(v) => self.make_member_def(source, v),
458 }
459 }
460
461 fn make_member_def(&self, source: &str, member_def: &MemberDef) -> (String, bool) {
462 let name = member_def.name();
463 let card = member_def.target_cardinality();
464 let target_type = member_def.target_type();
465 match &target_type {
466 TypeReference::Type(type_ref) => {
467 if *card == DEFAULT_CARDINALITY {
468 (
469 format!(" +{name}: {}\n", make_type_reference(target_type)),
470 true,
471 )
472 } else {
473 (
474 format!(
476 " s_{source} *--> \"+{name}\\n{}\" {}\n",
477 to_uml_string(card, true),
478 make_id(type_ref),
479 ),
480 false,
481 )
482 }
483 }
484 TypeReference::MappingType(_) => (
485 format!(
486 " +{name}: {} {}\n",
487 to_uml_string(card, false),
488 make_type_reference(target_type)
489 ),
490 true,
491 ),
492 TypeReference::Unknown => (format!(" +{name}: unknown\n"), true),
493 }
494 }
495
496 fn make_property_ref(&self, property_ref: &IdentifierReference) -> (String, bool) {
497 (format!(" +<<ref>> {property_ref}\n"), true)
498 }
499}
500
501#[inline(always)]
506fn make_id<S>(id: S) -> String
507where
508 S: Into<String>,
509{
510 format!("s_{}", id.into().replace(':', "__"))
511}
512
513fn start_type_with_sterotype(
514 type_class: &str,
515 type_name: &Identifier,
516 stereo_name: &str,
517) -> String {
518 format!(
519 " {} \"{}\" as {} << ({}, orchid) {} >> {{\n",
520 type_class,
521 type_name,
522 make_id(type_name),
523 stereo_name.chars().next().unwrap().to_uppercase(),
524 stereo_name
525 )
526}
527
528fn end_type(type_name: &Identifier, has_body: bool) -> String {
529 if !has_body {
530 format!(" }}\n hide {} members\n\n", make_id(type_name))
531 } else {
532 " }\n\n".to_string()
533 }
534}
535
536fn make_type_reference(type_ref: &TypeReference) -> String {
537 match type_ref {
538 TypeReference::Unknown => "unknown".to_string(),
539 TypeReference::Type(v) => v.to_string(),
540 TypeReference::MappingType(v) => format!(
541 "Mapping<{}, {}>",
542 make_type_reference(v.domain()),
543 make_type_reference(v.range()),
544 ),
545 }
546}
547
548fn make_imports(module: &Module) -> (String, String) {
549 let mut imports_top = String::new();
550 let mut imports_tail = String::new();
551 for other in module.imported_modules() {
552 imports_top.push_str(&format!(
553 "package \"{}\" as {} <<module>> #white {{\n",
554 other,
555 make_id(other)
556 ));
557 for imported in module
558 .imported_types()
559 .iter()
560 .filter(|qi| qi.module() == other)
561 {
562 imports_top.push_str(&format!(
563 " class \"{}\" as {}\n",
564 imported.member(),
565 make_id(*imported),
566 ));
567 }
568 let mut names = HashSet::default();
569 module.referenced_types(&mut names);
570 for imported in names
571 .iter()
572 .filter_map(|rt| rt.as_qualified_identifier())
573 .filter(|qi| qi.module() == other)
574 {
575 imports_top.push_str(&format!(
576 " class \"{}\" as {}\n",
577 imported.member(),
578 make_id(imported),
579 ));
580 }
581 imports_top.push_str("}\n\n");
582
583 imports_tail.push_str(&format!(
584 "{} ..> {}: <<import>>\n",
585 make_id(module.name()),
586 make_id(other)
587 ));
588 }
589 (imports_top, imports_tail)
590}
591
592#[inline(always)]
593fn format_to_arg(value: OutputFormat) -> CommandArg {
594 CommandArg::new(match value {
595 OutputFormat::ImageJpeg => "-tjpg",
596 OutputFormat::ImagePng => "-tpng",
597 OutputFormat::ImageSvg => "-tsvg",
598 _ => unreachable!(),
599 })
600}
601
602#[allow(dead_code)]
607#[inline(always)]
608fn path_to_output<P>(path: P, module_name: &Identifier) -> Result<DiagramOutput, Error>
609where
610 P: AsRef<Path>,
611{
612 let path = path.as_ref();
613 trace!("path_to_output({:?}, {})", path, module_name);
614
615 let output_dir = if path.components().count() == 1 {
616 std::env::current_dir()?.canonicalize()?
617 } else {
618 path.parent()
619 .unwrap() .canonicalize()?
621 };
622 trace!("path_to_output output_dir = {:?}", output_dir);
623
624 Ok(DiagramOutput {
625 file_name: path
626 .file_stem()
627 .map(|s| s.to_string_lossy().to_string())
628 .unwrap_or_else(|| module_name.to_string()),
629 output_dir: output_dir.to_string_lossy().to_string(),
630 })
631}
632
633fn to_uml_string(card: &Cardinality, as_association: bool) -> String {
634 let mut constraints: Vec<&str> = Vec::new();
635 if let Some(ordering) = card.ordering() {
636 if ordering == Ordering::Ordered {
637 constraints.push(KW_ORDERING_ORDERED);
638 }
639 }
640 if let Some(uniqueness) = card.uniqueness() {
641 if uniqueness == Uniqueness::Unique {
642 constraints.push(KW_UNIQUENESS_UNIQUE);
643 }
644 }
645 let constraints = if !constraints.is_empty() {
646 format!("{{{}}}", constraints.join(", "))
647 } else {
648 String::new()
649 };
650
651 let range_str = if card.range().is_range() {
652 format!(
653 "{}..{}",
654 card.range().min_occurs(),
655 card.range()
656 .max_occurs()
657 .map(|i| i.to_string())
658 .unwrap_or_else(|| String::from("*"))
659 )
660 } else {
661 card.range().min_occurs().to_string()
662 };
663
664 if constraints.is_empty() {
665 range_str
666 } else if as_association {
667 format!("{constraints}\\n{range_str}")
668 } else {
669 format!("[{range_str}] {constraints}")
670 }
671}