shapes_converter/shex_to_uml/
shex2uml.rs

1use std::{
2    fs::File,
3    io::{self, Write},
4    process::Command,
5};
6
7use prefixmap::{IriRef, PrefixMap, PrefixMapError};
8use shex_ast::{
9    Annotation, ObjectValue, Schema, Shape, ShapeExpr, ShapeExprLabel, TripleExpr, ValueSetValue,
10};
11use tracing::debug;
12
13use crate::{
14    find_annotation, object_value2string,
15    shex_to_uml::{ShEx2UmlConfig, ShEx2UmlError, Uml},
16};
17
18use super::{
19    Name, NodeId, UmlCardinality, UmlClass, UmlComponent, UmlEntry, ValueConstraint, PLANTUML,
20};
21use tempfile::TempDir;
22
23pub struct ShEx2Uml {
24    config: ShEx2UmlConfig,
25    current_uml: Uml,
26    current_prefixmap: PrefixMap,
27}
28
29impl ShEx2Uml {
30    pub fn new(config: &ShEx2UmlConfig) -> ShEx2Uml {
31        ShEx2Uml {
32            config: config.clone(),
33            current_uml: Uml::new(),
34            current_prefixmap: PrefixMap::new(),
35        }
36    }
37
38    pub fn as_plantuml<W: Write>(
39        &self,
40        writer: &mut W,
41        mode: &UmlGenerationMode,
42    ) -> Result<(), ShEx2UmlError> {
43        match mode {
44            UmlGenerationMode::AllNodes => {
45                self.current_uml.as_plantuml_all(&self.config, writer)?;
46                Ok(())
47            }
48            UmlGenerationMode::Neighs(str) => {
49                if let Some(node_id) = self.current_uml.get_node(str) {
50                    self.current_uml
51                        .as_plantuml_neighs(&self.config, writer, &node_id)?;
52                    Ok(())
53                } else {
54                    Err(ShEx2UmlError::NotFoundLabel { name: str.clone() })
55                }
56            }
57        }
58    }
59
60    /// Converts the current UML to an image
61    pub fn as_image<W: Write>(
62        &self,
63        writer: &mut W,
64        image_format: ImageFormat,
65        mode: &UmlGenerationMode,
66    ) -> Result<(), ShEx2UmlError> {
67        let tempdir = TempDir::new().map_err(|e| ShEx2UmlError::TempFileError { err: e })?;
68        let tempdir_path = tempdir.path();
69        let tempfile_path = tempdir_path.join("temp.uml");
70        let tempfile_name = tempfile_path.display().to_string();
71        let mut tempfile =
72            File::create(tempfile_path).map_err(|e| ShEx2UmlError::CreatingTempUMLFile {
73                tempfile_name: tempfile_name.clone(),
74                error: e,
75            })?;
76        self.as_plantuml(&mut tempfile, mode)?;
77        /*self.current_uml
78        .as_plantuml(&self.config, &mut tempfile, mode)?;*/
79        debug!("ShEx contents stored in temporary file:{}", tempfile_name);
80
81        let (out_param, out_file_name) = match image_format {
82            ImageFormat::PNG => ("-png", tempdir_path.join("temp.png")),
83            ImageFormat::SVG => ("-svg", tempdir_path.join("temp.svg")),
84        };
85        if let Some(plantuml_path) = &self.config.plantuml_path {
86            let mut command = Command::new("java");
87            command
88                .arg("-jar")
89                .arg(plantuml_path)
90                .arg("-o")
91                .arg(tempdir_path.to_string_lossy().to_string())
92                .arg(out_param)
93                .arg(tempfile_name);
94            let command_name = format!("{:?}", &command);
95            debug!("PLANTUML COMMAND:\n{command_name}");
96            let result = command.output();
97            match result {
98                Ok(_) => {
99                    let mut temp_file = File::open(out_file_name.as_path()).map_err(|e| {
100                        ShEx2UmlError::CantOpenGeneratedTempFile {
101                            generated_name: out_file_name.display().to_string(),
102                            error: e,
103                        }
104                    })?;
105                    copy(&mut temp_file, writer).map_err(|e| ShEx2UmlError::CopyingTempFile {
106                        temp_name: out_file_name.display().to_string(),
107                        error: e,
108                    })?;
109                    Ok(())
110                }
111                Err(e) => Err(ShEx2UmlError::PlantUMLCommandError {
112                    command: command_name,
113                    error: e,
114                }),
115            }
116        } else {
117            Err(ShEx2UmlError::NoPlantUMLPath {
118                env_name: PLANTUML.to_string(),
119            })
120        }
121    }
122
123    pub fn convert(&mut self, shex: &Schema) -> Result<(), ShEx2UmlError> {
124        self.current_prefixmap = shex.prefixmap().unwrap_or_default();
125        if let Some(shapes) = shex.shapes() {
126            for shape_decl in shapes {
127                let mut name = self.shape_label2name(&shape_decl.id)?;
128                let (node_id, _found) = self.current_uml.get_node_adding_label(&name.name());
129                let component =
130                    self.shape_expr2component(&mut name, &shape_decl.shape_expr, &node_id)?;
131                self.current_uml.update_component(node_id, component)?;
132            }
133        }
134        Ok(())
135    }
136
137    fn shape_label2name(&self, label: &ShapeExprLabel) -> Result<Name, ShEx2UmlError> {
138        match label {
139            ShapeExprLabel::IriRef { value } => {
140                iri_ref2name(value, &self.config, &None, &self.current_prefixmap)
141            }
142            ShapeExprLabel::BNode { value: _ } => todo!(),
143            ShapeExprLabel::Start => todo!(),
144        }
145    }
146
147    fn shape_expr2component(
148        &mut self,
149        name: &mut Name,
150        shape_expr: &ShapeExpr,
151        current_node_id: &NodeId,
152    ) -> Result<UmlComponent, ShEx2UmlError> {
153        match shape_expr {
154            ShapeExpr::Shape(shape) => self.shape2component(name, shape, current_node_id),
155            _ => Err(ShEx2UmlError::NotImplemented {
156                msg: "Complex shape expressions are not implemented yet".to_string(),
157            }),
158        }
159    }
160
161    fn shape2component(
162        &mut self,
163        name: &mut Name,
164        shape: &Shape,
165        current_node_id: &NodeId,
166    ) -> Result<UmlComponent, ShEx2UmlError> {
167        if let Some(label) = get_label(&shape.annotations, &self.current_prefixmap, &self.config)? {
168            name.add_label(label.as_str())
169        }
170        let mut uml_class = UmlClass::new(name.clone());
171        if let Some(extends) = &shape.extends {
172            for e in extends.iter() {
173                let extended_name = self.shape_label2name(e)?;
174                let (extended_node, found) = self
175                    .current_uml
176                    .get_node_adding_label(&extended_name.name());
177                self.current_uml
178                    .add_extends(current_node_id, &extended_node);
179                uml_class.add_extends(&extended_node);
180                if !found {
181                    self.current_uml.add_component(
182                        extended_node,
183                        UmlComponent::class(UmlClass::new(extended_name)),
184                    )?;
185                }
186            }
187        }
188        if let Some(te) = &shape.expression {
189            match &te.te {
190                TripleExpr::EachOf {
191                    id: _,
192                    expressions,
193                    min: _,
194                    max: _,
195                    sem_acts: _,
196                    annotations: _,
197                } => {
198                    for e in expressions {
199                        match &e.te {
200                            TripleExpr::TripleConstraint {
201                                id: _,
202                                negated: _,
203                                inverse: _,
204                                predicate,
205                                value_expr,
206                                min,
207                                max,
208                                sem_acts: _,
209                                annotations,
210                            } => {
211                                let pred_name = mk_name(
212                                    predicate,
213                                    annotations,
214                                    &self.config,
215                                    &self.current_prefixmap,
216                                )?;
217                                let card = mk_card(min, max)?;
218                                let value_constraint = if let Some(se) = value_expr {
219                                    self.value_expr2value_constraint(
220                                        se,
221                                        current_node_id,
222                                        &pred_name,
223                                        &card,
224                                    )?
225                                } else {
226                                    ValueConstraint::default()
227                                };
228                                match value_constraint {
229                                    ValueConstraint::None => {}
230                                    _ => {
231                                        let entry =
232                                            UmlEntry::new(pred_name, value_constraint, card);
233                                        uml_class.add_entry(entry)
234                                    }
235                                }
236                            }
237                            _ => todo!(),
238                        }
239                    }
240                }
241                TripleExpr::OneOf {
242                    id: _,
243                    expressions: _,
244                    min: _,
245                    max: _,
246                    sem_acts: _,
247                    annotations: _,
248                } => todo!(),
249                TripleExpr::TripleConstraint {
250                    id: _,
251                    negated: _,
252                    inverse: _,
253                    predicate,
254                    value_expr,
255                    min,
256                    max,
257                    sem_acts: _,
258                    annotations,
259                } => {
260                    let pred_name = mk_name(
261                        predicate,
262                        annotations,
263                        &self.config,
264                        &self.current_prefixmap,
265                    )?;
266                    let card = mk_card(min, max)?;
267                    let value_constraint = if let Some(se) = value_expr {
268                        self.value_expr2value_constraint(se, current_node_id, &pred_name, &card)?
269                    } else {
270                        ValueConstraint::default()
271                    };
272                    match value_constraint {
273                        ValueConstraint::None => {}
274                        _ => {
275                            let entry = UmlEntry::new(pred_name, value_constraint, card);
276                            uml_class.add_entry(entry)
277                        }
278                    }
279                }
280                TripleExpr::TripleExprRef(_) => todo!(),
281            }
282            Ok(UmlComponent::class(uml_class))
283        } else {
284            Ok(UmlComponent::class(uml_class))
285        }
286    }
287
288    fn value_expr2value_constraint(
289        &mut self,
290        value_expr: &ShapeExpr,
291        current_node_id: &NodeId,
292        current_predicate: &Name,
293        current_card: &UmlCardinality,
294    ) -> Result<ValueConstraint, ShEx2UmlError> {
295        match value_expr {
296            ShapeExpr::ShapeOr { shape_exprs: _ } => todo!(),
297            ShapeExpr::ShapeAnd { shape_exprs: _ } => todo!(),
298            ShapeExpr::ShapeNot { shape_expr: _ } => todo!(),
299            ShapeExpr::NodeConstraint(nc) => {
300                if let Some(datatype) = nc.datatype() {
301                    let name =
302                        iri_ref2name(&datatype, &self.config, &None, &self.current_prefixmap)?;
303                    Ok(ValueConstraint::datatype(name))
304                } else if let Some(value_set) = nc.values() {
305                    let value_set_constraint = value_set2value_constraint(
306                        &value_set,
307                        &self.config,
308                        &self.current_prefixmap,
309                    )?;
310                    Ok(ValueConstraint::ValueSet(value_set_constraint))
311                } else {
312                    todo!()
313                }
314            }
315            ShapeExpr::Shape(_) => todo!(),
316            ShapeExpr::External => todo!(),
317            ShapeExpr::Ref(r) => match &r {
318                ShapeExprLabel::IriRef { value } => {
319                    let ref_name =
320                        iri_ref2name(value, &self.config, &None, &self.current_prefixmap)?;
321                    self.current_uml.add_link(
322                        *current_node_id,
323                        ref_name,
324                        current_predicate.clone(),
325                        current_card.clone(),
326                    )?;
327                    Ok(ValueConstraint::None)
328                }
329                ShapeExprLabel::BNode { value: _ } => todo!(),
330                ShapeExprLabel::Start => todo!(),
331            },
332        }
333    }
334}
335
336fn value_set2value_constraint(
337    value_set: &Vec<ValueSetValue>,
338    config: &ShEx2UmlConfig,
339    prefixmap: &PrefixMap,
340) -> Result<Vec<Name>, ShEx2UmlError> {
341    let mut result = Vec::new();
342    for value in value_set {
343        match value {
344            ValueSetValue::ObjectValue(ObjectValue::IriRef(iri)) => {
345                let name = iri_ref2name(iri, config, &None, prefixmap)?;
346                result.push(name)
347            }
348            ValueSetValue::ObjectValue(ObjectValue::Literal(lit)) => {
349                return Err(ShEx2UmlError::not_implemented(
350                    format!("value_set2value_constraint with literal value: {lit:?}").as_str(),
351                ))
352            }
353            _ => {
354                return Err(ShEx2UmlError::not_implemented(
355                    format!("value_set2value_constraint with value: {value:?}").as_str(),
356                ))
357            }
358        }
359    }
360    Ok(result)
361}
362
363fn iri_ref2name(
364    iri_ref: &IriRef,
365    _config: &ShEx2UmlConfig,
366    maybe_label: &Option<String>,
367    prefixmap: &PrefixMap,
368) -> Result<Name, ShEx2UmlError> {
369    let mut name = match iri_ref {
370        IriRef::Iri(iri) => Name::new(prefixmap.qualify(iri).as_str(), Some(iri.as_str())),
371        IriRef::Prefixed { prefix, local } => {
372            let iri = prefixmap.resolve_prefix_local(prefix, local)?;
373            Name::new(format!("{prefix}:{local}").as_str(), Some(iri.as_str()))
374        }
375    };
376    if let Some(label) = maybe_label {
377        name.add_label(label)
378    };
379    Ok(name)
380}
381
382fn mk_card(min: &Option<i32>, max: &Option<i32>) -> Result<UmlCardinality, ShEx2UmlError> {
383    let min = if let Some(n) = min { *n } else { 1 };
384    let max = if let Some(n) = max { *n } else { 1 };
385    match (min, max) {
386        (1, 1) => Ok(UmlCardinality::OneOne),
387        (0, -1) => Ok(UmlCardinality::Star),
388        (0, 1) => Ok(UmlCardinality::Optional),
389        (1, -1) => Ok(UmlCardinality::Plus),
390        (m, n) if m >= 0 && n > m => Ok(UmlCardinality::Fixed(m)),
391        (m, n) if m >= 0 && n > m => Ok(UmlCardinality::Range(m, n)),
392        _ => Err(ShEx2UmlError::WrongCardinality { min, max }),
393    }
394}
395
396fn copy<W: Write>(file: &mut File, writer: &mut W) -> Result<(), io::Error> {
397    io::copy(file, writer)?;
398    Ok(())
399}
400
401fn mk_name(
402    iri: &IriRef,
403    annotations: &Option<Vec<Annotation>>,
404    config: &ShEx2UmlConfig,
405    prefixmap: &PrefixMap,
406) -> Result<Name, ShEx2UmlError> {
407    let label = get_label(annotations, prefixmap, config)?;
408    let name = iri_ref2name(iri, config, &label, prefixmap)?;
409    Ok(name)
410}
411
412fn get_label(
413    annotations: &Option<Vec<Annotation>>,
414    prefixmap: &PrefixMap,
415    config: &ShEx2UmlConfig,
416) -> Result<Option<String>, PrefixMapError> {
417    for label in config.annotation_label.iter() {
418        if let Some(value) = find_annotation(annotations, label, prefixmap)? {
419            return Ok(Some(object_value2string(&value)));
420        }
421    }
422    Ok(None)
423}
424
425pub enum ImageFormat {
426    SVG,
427    PNG,
428}
429
430#[derive(Debug, Clone, Default)]
431pub enum UmlGenerationMode {
432    /// Show all nodes
433    #[default]
434    AllNodes,
435
436    /// Show only the neighbours of a node
437    Neighs(String),
438}
439
440impl UmlGenerationMode {
441    pub fn all() -> UmlGenerationMode {
442        UmlGenerationMode::AllNodes
443    }
444
445    pub fn neighs(node: &str) -> UmlGenerationMode {
446        UmlGenerationMode::Neighs(node.to_string())
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    // use super::*;
453    // use shex_compact::ShExParser;
454
455    /*    #[test]
456        fn test_simple() {
457            let shex_str = "\
458    prefix : <http://example.org/>
459    prefix xsd: <http://www.w3.org/2001/XMLSchema#>
460
461    :Person {
462      :name xsd:string ;
463      :knows @:Person  ;
464      :works_for @:Course * ;
465    }
466
467    :Course {
468      :name xsd:string
469    }";
470            let mut expected_uml = Uml::new();
471            expected_uml.add_label(Name::new(":Person", Some("http://example.org/Person")));
472            expected_uml.add_label(Name::new(":Course", Some("http://example.org/Course")));
473            let shex = ShExParser::parse(shex_str, None).unwrap();
474            let converter = ShEx2Uml::new(ShEx2UmlConfig::default());
475            let converted_uml = converter.convert(&shex).unwrap();
476            assert_eq!(converted_uml, expected_uml);
477        } */
478}