sdml_generate/draw/
concepts.rs1use 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#[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
57impl 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
77impl 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
222fn to_uml_string(card: &Cardinality) -> String {
227 card.range().to_string()
228}