sdml_generate/draw/
uml.rs

1/*!
2Provide a generator for UML class diagrams via PlantUML.
3
4*/
5
6use 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// ------------------------------------------------------------------------------------------------
32// Public Types
33// ------------------------------------------------------------------------------------------------
34
35#[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// ------------------------------------------------------------------------------------------------
53// Private Types
54// ------------------------------------------------------------------------------------------------
55
56#[derive(Clone, Debug, Default)]
57struct DiagramOutput {
58    file_name: String,
59    output_dir: String,
60}
61
62// ------------------------------------------------------------------------------------------------
63// Implementations
64// ------------------------------------------------------------------------------------------------
65
66impl 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
89// ------------------------------------------------------------------------------------------------
90
91impl 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                // TODO: use path parameter instead!
122                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            // TODO: add opaque as stereotype on restriction
216
217            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                        // TODO: make references to entities "o" not "*"
475                        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// ------------------------------------------------------------------------------------------------
502// Private Functions
503// ------------------------------------------------------------------------------------------------
504
505#[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/// Note:
603///  PlantUML does not take output file names, it derives the names from the input file names.
604///  However, it will take the path of the directory to put output files in, which needs to be
605///  specified else it is derived from the input path (a temp file name).
606#[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() // safe due to test above
620            .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}