sdml_generate/draw/
concepts.rs

1/*!
2Provide a generator for "concept" diagrams via GraphViz.
3
4# Example
5
6```rust,no_run
7use sdml_core::store::InMemoryModuleCache;
8use sdml_core::model::modules::Module;
9use sdml_generate::Generator;
10use sdml_generate::draw::concepts::{ConceptDiagramGenerator, ConceptDiagramOptions};
11use std::io::stdout;
12# use sdml_core::model::identifiers::Identifier;
13# fn load_module() -> (Module, InMemoryModuleCache) { (Module::empty(Identifier::new_unchecked("example")), InMemoryModuleCache::default()) }
14
15let (module, cache) = load_module();
16
17let mut generator = ConceptDiagramGenerator::default();
18generator.generate(&module, &cache, None, &mut stdout()).expect("write to stdout failed");
19```
20
21*/
22
23use crate::draw::{
24    filter::{DefinitionKind, DiagramContentFilter},
25    OutputFormat, DOT_PROGRAM,
26};
27use crate::exec::exec_with_temp_input;
28use crate::Generator;
29use sdml_core::error::Error;
30use sdml_core::model::definitions::Definition;
31use sdml_core::model::definitions::HasMembers;
32use sdml_core::model::identifiers::IdentifierReference;
33use sdml_core::model::members::MemberKind;
34use sdml_core::model::members::{Cardinality, TypeReference, DEFAULT_CARDINALITY};
35use sdml_core::model::modules::Module;
36use sdml_core::model::{HasBody, HasName, HasOptionalBody};
37use sdml_core::store::ModuleStore;
38use std::collections::HashSet;
39use std::io::Write;
40use std::path::PathBuf;
41
42// ------------------------------------------------------------------------------------------------
43// Public Types
44// ------------------------------------------------------------------------------------------------
45
46#[derive(Debug, Default)]
47pub struct ConceptDiagramGenerator {
48    options: ConceptDiagramOptions,
49}
50
51#[derive(Debug, Default)]
52pub struct ConceptDiagramOptions {
53    content_filter: DiagramContentFilter,
54    output_format: OutputFormat,
55}
56
57// ------------------------------------------------------------------------------------------------
58// Implementations
59// ------------------------------------------------------------------------------------------------
60
61impl ConceptDiagramOptions {
62    pub fn with_content_filter(self, content_filter: DiagramContentFilter) -> Self {
63        Self {
64            content_filter,
65            ..self
66        }
67    }
68
69    pub fn with_output_format(self, output_format: OutputFormat) -> Self {
70        Self {
71            output_format,
72            ..self
73        }
74    }
75}
76
77// ------------------------------------------------------------------------------------------------
78
79impl Generator for ConceptDiagramGenerator {
80    type Options = ConceptDiagramOptions;
81
82    fn generate_with_options<W>(
83        &mut self,
84        module: &Module,
85        cache: &impl ModuleStore,
86        options: Self::Options,
87        _: Option<PathBuf>,
88        writer: &mut W,
89    ) -> Result<(), Error>
90    where
91        W: Write + Sized,
92    {
93        self.options = options;
94
95        let mut buffer = Vec::new();
96        write_module(module, cache, &self.options.content_filter, &mut buffer)?;
97
98        if self.options.output_format == OutputFormat::Source {
99            writer.write_all(&buffer)?;
100        } else {
101            let source = String::from_utf8(buffer).unwrap();
102            match exec_with_temp_input(DOT_PROGRAM, vec![self.options.output_format.into()], source)
103            {
104                Ok(result) => {
105                    writer.write_all(result.as_bytes())?;
106                }
107                Err(e) => {
108                    panic!("exec_with_input failed: {:?}", e);
109                }
110            }
111        }
112
113        Ok(())
114    }
115}
116
117fn write_module(
118    me: &Module,
119    cache: &impl ModuleStore,
120    content_filter: &DiagramContentFilter,
121    writer: &mut dyn Write,
122) -> Result<(), Error> {
123    writer.write_all(
124        r#"digraph G {
125  bgcolor="transparent";
126  rankdir="TB";
127  fontname="Helvetica,Arial,sans-serif";
128  node [fontname="Helvetica,Arial,sans-serif"; fontsize=10];
129  edge [fontname="Helvetica,Arial,sans-serif"; fontsize=9; fontcolor="dimgrey";
130        labelfontcolor="blue"; labeldistance=2.0];
131
132"#
133        .as_bytes(),
134    )?;
135
136    let mut entities: HashSet<String> = Default::default();
137    let mut relations: Vec<String> = Default::default();
138    for entity in me.body().entity_definitions() {
139        if content_filter.draw_definition_named(DefinitionKind::Entity, entity.name()) {
140            let current = entity.name().to_string();
141            entities.insert(current.clone());
142
143            if let Some(body) = entity.body() {
144                for member in body.members() {
145                    let (member_name, member_type) = match member.kind() {
146                        MemberKind::Reference(v) => {
147                            if let Some(Definition::Property(property)) = match &v {
148                                IdentifierReference::Identifier(v) => me.resolve_local(v),
149                                IdentifierReference::QualifiedIdentifier(v) => cache.resolve(v),
150                            } {
151                                (
152                                    property.member_def().name(),
153                                    property.member_def().target_type(),
154                                )
155                            } else {
156                                panic!()
157                            }
158                        }
159                        MemberKind::Definition(v) => (v.name(), v.target_type()),
160                    };
161                    let definition = match member_type {
162                        TypeReference::Type(IdentifierReference::Identifier(v)) => {
163                            me.resolve_local(v)
164                        }
165                        TypeReference::Type(IdentifierReference::QualifiedIdentifier(v)) => {
166                            cache.resolve(v)
167                        }
168                        _ => panic!(),
169                    };
170                    if let Some(Definition::Entity(entity)) = definition {
171                        entities.insert(entity.name().to_string());
172                        if let Some(property_name) = member.as_property_reference() {
173                            relations.push(format!(
174                        "  {current} -> {} [label=\"{}\";dir=\"both\";arrowtail=\"teetee\";arrowhead=\"teetee\"];\n",
175                        property_name,
176                        member_name,
177                    ));
178                        } else if let Some(definition) = member.as_definition() {
179                            if matches!(definition.target_type(), TypeReference::Unknown) {
180                                entities.insert("unknown".to_string());
181                            }
182                            let target_type = if let TypeReference::Type(target_type) =
183                                definition.target_type()
184                            {
185                                target_type.to_string().to_lowercase()
186                            } else {
187                                "unknown".to_string()
188                            };
189                            let target_cardinality = definition.target_cardinality();
190                            let head_str = if *target_cardinality == DEFAULT_CARDINALITY {
191                                String::new()
192                            } else {
193                                to_uml_string(target_cardinality)
194                            };
195                            relations.push(format!(
196                        "  {current} -> {target_type} [label=\"{}\"; headlabel=\"{head_str}\"];\n",
197                        member_name
198                    ));
199                        }
200                    }
201                }
202            }
203        }
204    }
205
206    writer.write_all(
207        entities
208            .iter()
209            .map(|name| format!("  {name} [label=\"{name}\"];"))
210            .collect::<Vec<String>>()
211            .join("\n")
212            .as_bytes(),
213    )?;
214
215    writer.write_all(relations.join("\n").as_bytes())?;
216
217    writer.write_all(b"}\n")?;
218
219    Ok(())
220}
221
222// ------------------------------------------------------------------------------------------------
223// Private Functions
224// ------------------------------------------------------------------------------------------------
225
226fn to_uml_string(card: &Cardinality) -> String {
227    card.range().to_string()
228}