Skip to main content

mdmodels_core/
exporters.rs

1/*
2 * Copyright (c) 2025 Jan Range
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 *
22 */
23
24use std::{collections::HashMap, error::Error, fmt::Display, str::FromStr};
25
26use crate::{
27    attribute::{Attribute, DataType},
28    markdown::frontmatter::FrontMatter,
29    object::Object,
30    option::AttrOption,
31    prelude::DataModel,
32    tree,
33    xmltype::XMLType,
34};
35use clap::ValueEnum;
36use colored::Colorize;
37use convert_case::{Case, Casing};
38use lazy_static::lazy_static;
39use minijinja::{
40    context,
41    value::{Kwargs, ValueKind, ViaDeserialize},
42    Environment, Value,
43};
44use textwrap::wrap;
45
46#[cfg(feature = "python")]
47use pyo3::pyclass;
48
49#[cfg(feature = "wasm")]
50use wasm_bindgen::prelude::wasm_bindgen;
51
52lazy_static! {
53    /// Maps generic type names to Python-specific type names.
54    static ref PYTHON_TYPE_MAPS: std::collections::HashMap<String, String> = {
55        let mut m = std::collections::HashMap::new();
56        m.insert("string".to_string(), "str".to_string());
57        m.insert("integer".to_string(), "int".to_string());
58        m.insert("boolean".to_string(), "bool".to_string());
59        m.insert("number".to_string(), "float".to_string());
60        m
61    };
62
63    /// Maps Python-specific type names to XSD-specific type names.
64    static ref XSD_TYPE_MAPS: std::collections::HashMap<String, String> = {
65        let mut m = std::collections::HashMap::new();
66        m.insert("str".to_string(), "string".to_string());
67        m.insert("bytes".to_string(), "base64Binary".to_string());
68        m
69    };
70
71    /// Maps MD-Models type names to Typescript-specific type names.
72    static ref TYPESCRIPT_TYPE_MAPS: std::collections::HashMap<String, String> = {
73        let mut m = std::collections::HashMap::new();
74        m.insert("integer".to_string(), "number".to_string());
75        m.insert("float".to_string(), "number".to_string());
76        m.insert("date".to_string(), "string".to_string());
77        m.insert("bytes".to_string(), "string".to_string());
78        m
79    };
80
81    /// Maps MD-Models type names to GraphQL-specific type names.
82    static ref GRAPHQL_TYPE_MAPS: std::collections::HashMap<String, String> = {
83        let mut m = std::collections::HashMap::new();
84        m.insert("integer".to_string(), "Int".to_string());
85        m.insert("number".to_string(), "Float".to_string());
86        m.insert("float".to_string(), "Float".to_string());
87        m.insert("boolean".to_string(), "Boolean".to_string());
88        m.insert("string".to_string(), "String".to_string());
89        m.insert("bytes".to_string(), "String".to_string());
90        m.insert("date".to_string(), "String".to_string());
91        m
92    };
93
94    /// Maps MD-Models type names to Owl-specific type names.
95    static ref OWL_TYPE_MAPS: std::collections::HashMap<String, String> = {
96        let mut m = std::collections::HashMap::new();
97        m.insert("integer".to_string(), "xsd:integer".to_string());
98        m.insert("number".to_string(), "xsd:decimal".to_string());
99        m.insert("float".to_string(), "xsd:decimal".to_string());
100        m.insert("boolean".to_string(), "xsd:boolean".to_string());
101        m.insert("string".to_string(), "xsd:string".to_string());
102        m.insert("bytes".to_string(), "xsd:base64Binary".to_string());
103        m.insert("date".to_string(), "xsd:date".to_string());
104        m
105    };
106
107    /// Forbidden enum variants for Rust (mainly for windows compatibility)
108    static ref FORBIDDEN_RUST_ENUM_VARIANTS: Vec<String> = {
109        vec![
110            "yield".to_string(),
111        ]
112    };
113}
114
115/// Enumeration of available templates.
116#[derive(Debug, ValueEnum, Clone, PartialEq)]
117#[cfg_attr(feature = "python", pyclass(eq, eq_int, from_py_object))]
118#[cfg_attr(feature = "wasm", wasm_bindgen)]
119pub enum Templates {
120    /// XML Schema
121    XmlSchema,
122    /// Markdown
123    Markdown,
124    /// Compact Markdown
125    CompactMarkdown,
126    /// JSON Schema
127    JsonSchema,
128    /// JSON Schema All
129    JsonSchemaAll,
130    /// JSON-LD
131    JsonLd,
132    /// SHACL
133    Shacl,
134    /// OWL
135    Owl,
136    /// ShEx
137    Shex,
138    /// Python Dataclass
139    PythonDataclass,
140    /// Python Pydantic XML
141    PythonPydanticXML,
142    /// Python Pydantic
143    PythonPydantic,
144    /// MkDocs
145    MkDocs,
146    /// Internal
147    Internal,
148    /// Typescript (io-ts)
149    Typescript,
150    /// Typescript (Zod)
151    TypescriptZod,
152    /// Rust
153    Rust,
154    /// Protobuf
155    Protobuf,
156    /// Graphql
157    Graphql,
158    /// Golang
159    Golang,
160    /// Linkml
161    Linkml,
162    /// Julia
163    Julia,
164    /// Mermaid class diagram
165    Mermaid,
166}
167
168impl Display for Templates {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        match self {
171            Templates::PythonDataclass => write!(f, "python-dataclass"),
172            Templates::PythonPydantic => write!(f, "python-pydantic"),
173            Templates::PythonPydanticXML => write!(f, "python-pydantic-xml"),
174            Templates::XmlSchema => write!(f, "xml-schema"),
175            Templates::Markdown => write!(f, "markdown"),
176            Templates::CompactMarkdown => write!(f, "compact-markdown"),
177            Templates::Shacl => write!(f, "shacl"),
178            Templates::JsonSchema => write!(f, "json-schema"),
179            Templates::JsonSchemaAll => write!(f, "json-schema-all"),
180            Templates::JsonLd => write!(f, "json-ld"),
181            Templates::Shex => write!(f, "shex"),
182            Templates::MkDocs => write!(f, "mk-docs"),
183            Templates::Internal => write!(f, "internal"),
184            Templates::Typescript => write!(f, "typescript"),
185            Templates::TypescriptZod => write!(f, "typescript-zod"),
186            Templates::Rust => write!(f, "rust"),
187            Templates::Protobuf => write!(f, "protobuf"),
188            Templates::Graphql => write!(f, "graphql"),
189            Templates::Golang => write!(f, "golang"),
190            Templates::Linkml => write!(f, "linkml"),
191            Templates::Julia => write!(f, "julia"),
192            Templates::Mermaid => write!(f, "mermaid"),
193            Templates::Owl => write!(f, "owl"),
194        }
195    }
196}
197
198/// Converts string representation of a template to a `Templates` enum.
199/// and returns an error if the string is not a valid template type.
200impl FromStr for Templates {
201    type Err = Box<dyn Error>;
202    fn from_str(s: &str) -> Result<Self, Box<dyn Error>> {
203        match s {
204            "python-dataclass" => Ok(Templates::PythonDataclass),
205            "python-sdrdm" => Ok(Templates::PythonPydanticXML),
206            "python-pydantic" => Ok(Templates::PythonPydantic),
207            "python-pydantic-xml" => Ok(Templates::PythonPydanticXML),
208            "xml-schema" => Ok(Templates::XmlSchema),
209            "markdown" => Ok(Templates::Markdown),
210            "compact-markdown" => Ok(Templates::CompactMarkdown),
211            "shacl" => Ok(Templates::Shacl),
212            "json-schema" => Ok(Templates::JsonSchema),
213            "json-schema-all" => Ok(Templates::JsonSchemaAll),
214            "shex" => Ok(Templates::Shex),
215            "mk-docs" => Ok(Templates::MkDocs),
216            "internal" => Ok(Templates::Internal),
217            "typescript" => Ok(Templates::Typescript),
218            "typescript-zod" => Ok(Templates::TypescriptZod),
219            "rust" => Ok(Templates::Rust),
220            "protobuf" => Ok(Templates::Protobuf),
221            "graphql" => Ok(Templates::Graphql),
222            "golang" => Ok(Templates::Golang),
223            "linkml" => Ok(Templates::Linkml),
224            "julia" => Ok(Templates::Julia),
225            "mermaid" => Ok(Templates::Mermaid),
226            "owl" => Ok(Templates::Owl),
227            _ => {
228                let err = format!("Invalid template type: {s}");
229                Err(err.into())
230            }
231        }
232    }
233}
234
235/// Renders a Jinja template based on the provided template type and data model.
236///
237/// # Arguments
238///
239/// * `template` - The type of template to render.
240/// * `model` - The data model to use for rendering the template.
241///
242/// # Returns
243///
244/// A Result containing the rendered template as a String or an error if rendering fails.
245pub fn render_jinja_template(
246    template: &Templates,
247    model: &mut DataModel,
248    config: Option<&HashMap<String, String>>,
249) -> Result<String, minijinja::Error> {
250    // Load the template environment
251    let mut env = Environment::new();
252    minijinja_embed::load_templates!(&mut env);
253
254    // Keep track of fields that are artificially added,
255    // but not part of the original model. Mainly used for
256    // Database migrations and objects which do not have
257    // a primary key.
258    let mut artificial_fields = HashMap::new();
259
260    // If there is no config, create an empty one
261    // This is necessary to avoid errors when rendering the template
262    let config = config.cloned().unwrap_or_default();
263
264    // Perform type conversions and filtering based on the template
265    match template {
266        Templates::XmlSchema => convert_model_types(model, &XSD_TYPE_MAPS),
267        Templates::Typescript => convert_model_types(model, &TYPESCRIPT_TYPE_MAPS),
268        Templates::Graphql => convert_model_types(model, &GRAPHQL_TYPE_MAPS),
269        Templates::Shacl | Templates::Shex => {
270            convert_model_types(model, &OWL_TYPE_MAPS);
271            if let Err(e) = filter_objects_wo_terms(model) {
272                println!(
273                    " [{}] {}",
274                    template.to_string().yellow().bold(),
275                    e.to_string().bold(),
276                );
277            }
278        }
279        Templates::Owl => {
280            convert_model_types(model, &OWL_TYPE_MAPS);
281            remove_default_prefixes(model);
282        }
283        Templates::PythonDataclass | Templates::PythonPydanticXML | Templates::PythonPydantic => {
284            convert_astropy_types(model, &config);
285            convert_model_types(model, &PYTHON_TYPE_MAPS);
286            sort_attributes_by_required(model);
287        }
288        Templates::Julia => {
289            sort_by_dependency(model);
290        }
291        Templates::Golang => {
292            if config.contains_key("gorm") {
293                add_id_pks(model, &mut artificial_fields);
294            }
295        }
296        Templates::Rust => {
297            check_for_forbidden_rust_enum_variants(model);
298        }
299        _ => {}
300    }
301
302    // Add custom functions to the Jinja environment
303    env.add_function("wrap", wrap_text);
304    env.add_function("replace", replace);
305    env.add_function("trim", trim);
306    env.add_function("default_value", default_value);
307    env.add_filter("enumerate", enumerate);
308    env.add_filter("cap_first", cap_first);
309    env.add_filter("split_path_pairs", split_path_pairs);
310    env.add_filter("pascal_case", pascal_case);
311    env.add_filter("camel_case", camel_case);
312    env.add_filter("snake_case", snake_case);
313    env.add_filter("replace_lower", replace_lower);
314
315    // Get the appropriate template
316    let template = match template {
317        Templates::PythonDataclass => env.get_template("python-dataclass.jinja")?,
318        Templates::PythonPydantic => env.get_template("python-pydantic.jinja")?,
319        Templates::XmlSchema => env.get_template("xml-schema.jinja")?,
320        Templates::Markdown => env.get_template("markdown.jinja")?,
321        Templates::CompactMarkdown => env.get_template("markdown-compact.jinja")?,
322        Templates::Shacl => env.get_template("shacl.jinja")?,
323        Templates::Shex => env.get_template("shex.jinja")?,
324        Templates::PythonPydanticXML => env.get_template("python-pydantic-xml.jinja")?,
325        Templates::MkDocs => env.get_template("mkdocs.jinja")?,
326        Templates::Typescript => env.get_template("typescript.jinja")?,
327        Templates::TypescriptZod => env.get_template("typescript-zod.jinja")?,
328        Templates::Rust => env.get_template("rust.jinja")?,
329        Templates::Protobuf => env.get_template("protobuf.jinja")?,
330        Templates::Graphql => env.get_template("graphql.jinja")?,
331        Templates::Golang => env.get_template("golang.jinja")?,
332        Templates::Julia => env.get_template("julia.jinja")?,
333        Templates::Mermaid => env.get_template("mermaid.jinja")?,
334        Templates::Owl => env.get_template("owl.jinja")?,
335        _ => {
336            panic!(
337                "The template is not available as a Jinja Template and should not be used using the jinja exporter.
338                Instead, use the dedicated exporter in the DataModel struct (e.g. `DataModel::json_ld_header`, DataModel::json_schema)."
339            )
340        }
341    };
342
343    // If there is no config, create an empty one
344    // This is necessary to avoid errors when rendering the template
345    if model.config.is_none() {
346        model.config = Some(FrontMatter::default());
347    }
348
349    // Render the template
350    let prefixes = get_prefixes(model);
351    let rendered = template.render(context! {
352        objects => model.objects,
353        object_names => model.objects.iter().map(|o| o.name.clone()).collect::<Vec<String>>(),
354        enums => model.enums,
355        enum_names => model.enums.iter().map(|e| e.name.clone()).collect::<Vec<String>>(),
356        title => model.name,
357        prefixes => prefixes,
358        repo => model.config.as_ref().unwrap().repo.clone(),
359        prefix => model.config.as_ref().unwrap().prefix.clone(),
360        nsmap => model.config.as_ref().unwrap().nsmap.clone(),
361        config => config,
362        objects_with_wrapped => get_objects_with_wrapped(model),
363        pk_objects => pk_objects(model),
364        artificial_fields => artificial_fields,
365        has_union_types => has_union_types(model),
366    });
367
368    match rendered {
369        Ok(r) => Ok(clean_and_trim(&r)),
370        Err(e) => Err(e),
371    }
372}
373
374/// Returns a vector of object names that have attributes with XML wrapped types.
375///
376/// # Arguments
377///
378/// * `model` - The data model to search for wrapped objects
379///
380/// # Returns
381///
382/// A vector of strings containing the names of objects that have attributes with XML wrapped types
383fn get_objects_with_wrapped(model: &mut DataModel) -> Vec<String> {
384    model
385        .objects
386        .iter()
387        .filter(|o| {
388            o.attributes.iter().any(|a| {
389                if let Some(xml) = &a.xml {
390                    matches!(xml, XMLType::Wrapped { .. })
391                } else {
392                    false
393                }
394            })
395        })
396        .map(|o| o.name.clone())
397        .collect()
398}
399
400/// Replaces all occurrences of a substring with another substring.
401///
402/// # Arguments
403///
404/// * `value` - The string to perform replacements on
405/// * `from` - The substring to replace
406/// * `to` - The substring to replace with
407///
408/// # Returns
409///
410/// A new string with all occurrences of `from` replaced with `to`
411fn replace(value: String, from: &str, to: &str) -> String {
412    value.replace(from, to)
413}
414
415/// Replaces all occurrences of a substring with another substring and converts the result to lowercase.
416///
417/// # Arguments
418///
419/// * `value` - The string to perform replacements on
420/// * `from` - The substring to replace
421/// * `to` - The substring to replace with
422///
423/// # Returns
424///
425/// A new string with all occurrences of `from` replaced with `to` and converted to lowercase
426fn replace_lower(value: String, from: String, to: String) -> String {
427    value.replace(&from, &to).to_lowercase()
428}
429
430/// Template function that allows to wrap text at a certain length.
431///
432/// # Arguments
433///
434/// * `text` - The text to wrap.
435/// * `width` - The maximum length of a line.
436/// * `offset` - The offset to use for all lines.
437///
438/// # Returns
439///
440/// A string with the wrapped text.
441fn wrap_text(
442    text: &str,
443    width: usize,
444    initial_offset: &str,
445    offset: &str,
446    delimiter: Option<&str>,
447) -> String {
448    let delimiter = delimiter.unwrap_or("");
449    // Remove multiple spaces
450    let options = textwrap::Options::new(width)
451        .initial_indent(initial_offset)
452        .subsequent_indent(offset)
453        .width(width)
454        .break_words(false);
455
456    wrap(remove_multiple_spaces(text).as_str(), options).join(&format!("{delimiter}\n"))
457}
458
459/// Splits a path into pairs of (current, previous) components.
460///
461/// # Arguments
462///
463/// * `path` - The path to split, using '/' as separator
464/// * `initial` - The initial previous value to use for the first component
465///
466/// # Returns
467///
468/// A vector of tuples containing (current_component, previous_component)
469fn split_path_pairs(path: String, initial: Option<String>) -> Vec<Vec<String>> {
470    let initial = initial.unwrap_or_default();
471    let parts: Vec<&str> = path.split('/').collect();
472    let mut pairs = Vec::new();
473    let mut prev = initial;
474
475    for part in parts {
476        if !part.is_empty() {
477            pairs.push(vec![part.to_string(), prev.clone()]);
478            prev = part.to_string();
479        }
480    }
481
482    pairs
483}
484
485/// Filter use only for Jinja templates.
486/// Converts a string to PascalCase.
487fn pascal_case(s: String) -> String {
488    if s.ends_with("_") {
489        s.to_case(Case::Pascal) + "_"
490    } else {
491        s.to_case(Case::Pascal)
492    }
493}
494
495/// Filter use only for Jinja templates.
496/// Converts a string to camelCase.
497fn camel_case(s: String) -> String {
498    s.to_case(Case::Camel)
499}
500
501/// Filter use only for Jinja templates.
502/// Converts a string to snake_case.
503fn snake_case(s: String) -> String {
504    s.to_case(Case::Snake)
505}
506
507/// Removes leading and trailing whitespace and multiple spaces from a string.
508fn remove_multiple_spaces(input: &str) -> String {
509    input.split_whitespace().collect::<Vec<&str>>().join(" ")
510}
511
512/// Removes trailing underscores from a string.
513fn trim(input: &str, prefix: &str) -> String {
514    input
515        .trim_start_matches(prefix)
516        .trim_end_matches(prefix)
517        .to_string()
518}
519
520/// Checks if an object has a primary key.
521fn pk_objects(model: &mut DataModel) -> HashMap<String, (String, String, bool)> {
522    let mut pk_objects = HashMap::new();
523    for object in &mut model.objects {
524        for attribute in &object.attributes {
525            for option in &attribute.options {
526                if let AttrOption::PrimaryKey(true) = option {
527                    pk_objects.insert(
528                        object.name.clone(),
529                        (attribute.name.clone(), attribute.dtypes[0].clone(), true),
530                    );
531                    break;
532                }
533            }
534        }
535    }
536    pk_objects
537}
538
539/// Converts the data types in the model according to the provided type map.
540///
541/// # Arguments
542///
543/// * `model` - The data model whose types are to be converted.
544/// * `type_map` - A map of generic type names to specific type names.
545fn convert_model_types(
546    model: &mut DataModel,
547    type_map: &std::collections::HashMap<String, String>,
548) {
549    for object in &mut model.objects {
550        for attribute in &mut object.attributes {
551            attribute.dtypes = attribute
552                .dtypes
553                .iter()
554                .map(|t| type_map.get(t).unwrap_or(t))
555                .map(|t| t.to_string())
556                .collect();
557        }
558    }
559}
560
561/// Adds an ID field to objects in the model that don't have a primary key.
562///
563/// This function ensures that each object has a primary key by either:
564/// 1. Adding a new 'id' attribute if the object has no primary key and no 'id' field
565/// 2. Making an existing 'id' field a primary key if one exists but isn't already a primary key
566///
567/// # Arguments
568///
569/// * `model` - The data model to modify
570fn add_id_pks(model: &mut DataModel, artificially_added_fields: &mut HashMap<String, String>) {
571    for object in &mut model.objects {
572        match (has_primary_key(object), has_id(object)) {
573            (false, false) => {
574                object.attributes.insert(0, id_attribute());
575                artificially_added_fields.insert(object.name.clone(), "id".to_string());
576            }
577            (false, true) => {
578                object
579                    .attributes
580                    .iter_mut()
581                    .find(|a| a.name == "id")
582                    .unwrap()
583                    .options
584                    .push(AttrOption::PrimaryKey(true));
585            }
586            _ => {}
587        }
588    }
589}
590
591/// Creates a new ID attribute with primary key option.
592///
593/// This function creates a new Attribute instance representing an ID field
594/// with the following properties:
595/// - Name: "id"
596/// - Required: false
597/// - Type: integer
598/// - Primary key option enabled
599///
600/// # Returns
601///
602/// A new Attribute configured as an ID field
603fn id_attribute() -> Attribute {
604    let mut attr = Attribute::new("id".to_string(), true);
605    attr.options.push(AttrOption::PrimaryKey(true));
606    attr.set_dtype("integer".to_string()).unwrap();
607    attr
608}
609
610/// Checks if an object has any attribute marked as a primary key.
611///
612/// # Arguments
613///
614/// * `object` - The object to check for primary key attributes
615///
616/// # Returns
617///
618/// `true` if the object has an attribute with primary key option, `false` otherwise
619fn has_primary_key(object: &Object) -> bool {
620    object
621        .attributes
622        .iter()
623        .any(|a| a.options.iter().any(|o| o.key() == "primary key"))
624}
625
626/// Checks if an object has an attribute named "id".
627///
628/// # Arguments
629///
630/// * `object` - The object to check for an ID attribute
631///
632/// # Returns
633///
634/// `true` if the object has an attribute named "id", `false` otherwise
635fn has_id(object: &Object) -> bool {
636    object.attributes.iter().any(|a| a.name == "id")
637}
638
639/// Converts the data types in the model according to the provided type map.
640///
641/// # Arguments
642///
643/// * `model` - The data model whose types are to be converted.
644fn convert_astropy_types(model: &mut DataModel, config: &HashMap<String, String>) {
645    if !config.contains_key("astropy") {
646        return;
647    }
648
649    // Replace UnitDefinition with UnitDefinitionAnnot
650    for object in &mut model.objects {
651        for attribute in &mut object.attributes {
652            if attribute.dtypes.contains(&"UnitDefinition".to_string()) {
653                attribute.dtypes = vec!["UnitDefinitionAnnot".to_string()];
654            }
655        }
656    }
657
658    model
659        .objects
660        .retain(|o| o.name != "UnitDefinition" && o.name != "BaseUnit");
661
662    model.enums.retain(|e| e.name != "UnitType");
663}
664
665/// Retrieves the prefixes from the model configuration.
666///
667/// # Arguments
668///
669/// * `model` - The data model from which to retrieve the prefixes.
670///
671/// # Returns
672///
673/// A vector of prefix tuples (prefix, URI).
674fn get_prefixes(model: &mut DataModel) -> Vec<(String, String)> {
675    let mut prefixes = match &model.config {
676        Some(config) => config.prefixes().unwrap_or(vec![]),
677        None => vec![],
678    };
679
680    prefixes.sort_by(|a, b| a.0.cmp(&b.0));
681    prefixes
682}
683
684/// Filters out objects from the model that do not have any terms.
685///
686/// # Arguments
687///
688/// * `model` - The data model to filter.
689fn filter_objects_wo_terms(model: &mut DataModel) -> Result<(), Box<dyn Error>> {
690    model.objects.retain(|o| o.has_any_terms());
691
692    if model.objects.is_empty() {
693        return Err(Box::new(std::io::Error::new(
694            std::io::ErrorKind::InvalidInput,
695            "No objects with terms found in the model. Unable to build SHACL or ShEx.",
696        )));
697    }
698    Ok(())
699}
700
701/// Checks for forbidden Rust enum variants and replaces them with a valid variant.
702///
703/// # Arguments
704///
705/// * `model` - The data model to check for forbidden Rust enum variants.
706fn check_for_forbidden_rust_enum_variants(model: &mut DataModel) {
707    for enumeration in &mut model.enums {
708        enumeration.mappings = enumeration
709            .mappings
710            .iter()
711            .map(|(key, value)| {
712                let new_key = if FORBIDDEN_RUST_ENUM_VARIANTS.contains(&key.to_lowercase()) {
713                    format!("{key}_")
714                } else {
715                    key.to_lowercase()
716                };
717                (new_key, value.clone())
718            })
719            .collect();
720    }
721}
722
723/// Sorts the objects in the model by their dependency.
724///
725/// This is important for languages like Julia, where forward declarations
726/// are not supported.
727///
728/// # Arguments
729///
730/// * `model` - The data model whose objects are to be sorted.
731fn sort_by_dependency(model: &mut DataModel) {
732    let graph = tree::dependency_graph(model);
733    let mut class_order = tree::get_topological_order(&graph);
734    class_order.reverse();
735    model
736        .objects
737        .sort_by_key(|o| class_order.iter().position(|c| c == &o.name).unwrap());
738}
739
740/// Sorts the attributes of each object in the model by their 'required' field.
741///
742/// # Arguments
743///
744/// * `model` - The data model whose attributes are to be sorted.
745fn sort_attributes_by_required(model: &mut DataModel) {
746    for object in &mut model.objects {
747        object.sort_attrs_by_required();
748    }
749}
750
751/// Cleans and trims a string by removing trailing whitespace and limiting consecutive empty lines.
752///
753/// This function processes a string by:
754/// 1. Splitting it into lines
755/// 2. Trimming trailing whitespace from each line
756/// 3. Limiting consecutive empty lines to a maximum of 2
757/// 4. Joining the lines back together
758///
759/// # Arguments
760///
761/// * `s` - The string to clean and trim
762///
763/// # Returns
764///
765/// A cleaned string with trailing whitespace removed and consecutive empty lines limited
766fn clean_and_trim(s: &str) -> String {
767    let splitted = s.split('\n').collect::<Vec<&str>>();
768    let mut cleaned = vec![];
769    let mut consec_empty = 0;
770
771    for line in splitted {
772        let trimmed = line.trim_end();
773        if !trimmed.is_empty() {
774            cleaned.push(trimmed);
775            consec_empty = 0;
776        } else {
777            consec_empty += 1;
778            if consec_empty < 3 {
779                cleaned.push(trimmed);
780            }
781        }
782    }
783
784    cleaned.join("\n").trim().to_string()
785}
786
787// Enumerate a collection of objects
788pub fn enumerate(v: &Value, _: Kwargs) -> Result<Value, minijinja::Error> {
789    if v.kind() != ValueKind::Seq {
790        return Err(minijinja::Error::new(
791            minijinja::ErrorKind::InvalidOperation,
792            "Can only enumerate sequences",
793        ));
794    }
795
796    // Turn into iterator of (index, value)
797    Ok(v.try_iter()
798        .expect("Failed to iterate over sequence")
799        .enumerate()
800        .map(|(i, v)| Value::from(vec![Value::from(i), v]))
801        .collect())
802}
803
804/// Capitalizes the first character of a string.
805///
806/// # Arguments
807///
808/// * `s` - The string to capitalize
809///
810/// # Returns
811///
812/// A new string with the first character capitalized
813fn cap_first(s: String) -> String {
814    let mut chars = s.chars();
815    match chars.next() {
816        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
817        None => s.to_string(),
818    }
819}
820
821/// Formats the default value of an attribute.
822///
823/// # Arguments
824///
825/// * `default` - The default value of an attribute.
826///
827/// # Returns
828fn default_value(attribute: ViaDeserialize<Attribute>) -> String {
829    match &attribute.default {
830        Some(DataType::String(s)) => format!("\"{s}\""),
831        Some(DataType::Integer(i)) => {
832            if contains_numeric_type(&attribute) {
833                i.to_string()
834            } else {
835                format!("\"{i}\"")
836            }
837        }
838        Some(DataType::Float(f)) => {
839            if contains_numeric_type(&attribute) {
840                f.to_string()
841            } else {
842                format!("\"{f}\"")
843            }
844        }
845        _ => "".to_string(),
846    }
847}
848
849/// Checks if an attribute contains a numeric type.
850///
851/// # Arguments
852///
853/// * `attribute` - The attribute to check.
854///
855/// # Returns
856///
857/// `true` if the attribute contains a numeric type, `false` otherwise.
858fn contains_numeric_type(attribute: &Attribute) -> bool {
859    attribute
860        .dtypes
861        .iter()
862        .any(|t| t == "integer" || t == "float")
863}
864
865/// Checks if an object has multiple types.
866///
867/// # Arguments
868///
869/// * `object` - The object to check.
870///
871/// # Returns
872///
873/// `true` if the object has union types, `false` otherwise.
874fn has_union_types(model: &mut DataModel) -> bool {
875    model
876        .objects
877        .iter()
878        .any(|o| o.attributes.iter().any(|a| a.dtypes.len() > 1))
879}
880
881/// Removes the default prefixes from the model.
882///
883/// # Arguments
884///
885/// * `model` - The data model whose prefixes are to be removed.
886fn remove_default_prefixes(model: &mut DataModel) {
887    if let Some(prefixes) = &mut model.config.as_mut().unwrap().prefixes {
888        prefixes.remove("xsd");
889        prefixes.remove("rdfs");
890        prefixes.remove("owl");
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use pretty_assertions::assert_eq;
897    use std::fs;
898
899    use crate::markdown::parser::parse_markdown;
900
901    use super::*;
902
903    /// Helper function to build and convert a template.
904    ///
905    /// # Arguments
906    ///
907    /// * `template` - The template type to use for rendering.
908    ///
909    /// # Returns
910    ///
911    /// A string containing the rendered template.
912    fn build_and_convert(
913        path: &str,
914        template: Templates,
915        config: Option<&HashMap<String, String>>,
916    ) -> String {
917        let content = fs::read_to_string(path).expect("Could not read markdown file");
918        let mut model = parse_markdown(&content, None).expect("Failed to parse markdown file");
919        render_jinja_template(&template, &mut model, config)
920            .expect("Could not render template")
921            .to_string()
922    }
923
924    #[test]
925    fn test_convert_to_shex() {
926        // Arrange
927        let rendered = build_and_convert("tests/data/model.md", Templates::Shex, None);
928
929        // Assert
930        let expected = fs::read_to_string("tests/data/expected_shex.shex")
931            .expect("Could not read expected file");
932        assert_eq!(rendered, expected);
933    }
934
935    #[test]
936    fn test_convert_to_shacl() {
937        // Arrange
938        let rendered = build_and_convert("tests/data/model.md", Templates::Shacl, None);
939
940        // Assert
941        let expected = fs::read_to_string("tests/data/expected_shacl.ttl")
942            .expect("Could not read expected file");
943        assert_eq!(rendered, expected);
944    }
945
946    #[test]
947    fn test_convert_to_owl() {
948        // Arrange
949        let rendered = build_and_convert("tests/data/model.md", Templates::Owl, None);
950
951        // Assert
952        let expected = fs::read_to_string("tests/data/expected_owl.ttl")
953            .expect("Could not read expected file");
954        assert_eq!(rendered, expected);
955    }
956
957    #[test]
958    fn test_convert_to_python_dc() {
959        // Arrange
960        let rendered = build_and_convert("tests/data/model.md", Templates::PythonDataclass, None);
961
962        // Assert
963        let expected = fs::read_to_string("tests/data/expected_python_dc.py")
964            .expect("Could not read expected file");
965        assert_eq!(rendered, expected);
966    }
967
968    #[test]
969    fn test_convert_to_python_pydantic_xml() {
970        // Arrange
971        let rendered = build_and_convert("tests/data/model.md", Templates::PythonPydanticXML, None);
972
973        // Assert
974        let expected = fs::read_to_string("tests/data/expected_python_pydantic_xml.py")
975            .expect("Could not read expected file");
976        assert_eq!(rendered, expected);
977    }
978
979    #[test]
980    fn test_convert_to_xsd() {
981        // Arrange
982        let rendered = build_and_convert("tests/data/model.md", Templates::XmlSchema, None);
983
984        // Assert
985        let expected = fs::read_to_string("tests/data/expected_xml_schema.xsd")
986            .expect("Could not read expected file");
987        assert_eq!(rendered, expected);
988    }
989
990    #[test]
991    fn test_convert_to_mkdocs() {
992        // Arrange
993        let rendered = build_and_convert("tests/data/model.md", Templates::MkDocs, None);
994
995        // Assert
996        let expected = fs::read_to_string("tests/data/expected_mkdocs.md")
997            .expect("Could not read expected file");
998        assert_eq!(rendered, expected);
999    }
1000
1001    #[test]
1002    fn test_convert_to_typescript() {
1003        // Arrange
1004        let rendered = build_and_convert("tests/data/model.md", Templates::Typescript, None);
1005
1006        // Assert
1007        let expected = fs::read_to_string("tests/data/expected_typescript.ts")
1008            .expect("Could not read expected file");
1009        assert_eq!(rendered, expected);
1010    }
1011
1012    #[test]
1013    fn test_convert_to_typescript_zod() {
1014        // Arrange
1015        let rendered = build_and_convert("tests/data/model.md", Templates::TypescriptZod, None);
1016
1017        // Assert
1018        let expected = fs::read_to_string("tests/data/expected_typescript_zod.ts")
1019            .expect("Could not read expected file");
1020        assert_eq!(rendered, expected);
1021    }
1022
1023    #[test]
1024    fn test_convert_to_typescript_zod_json_ld() {
1025        // Arrange
1026        let rendered = build_and_convert(
1027            "tests/data/model.md",
1028            Templates::TypescriptZod,
1029            Some(&HashMap::from([(
1030                "json-ld".to_string(),
1031                "true".to_string(),
1032            )])),
1033        );
1034
1035        // Assert
1036        let expected = fs::read_to_string("tests/data/expected_typescript_zod_json_ld.ts")
1037            .expect("Could not read expected file");
1038        assert_eq!(rendered, expected);
1039    }
1040
1041    #[test]
1042    fn test_convert_to_pydantic() {
1043        // Arrange
1044        let rendered = build_and_convert("tests/data/model.md", Templates::PythonPydantic, None);
1045
1046        // Assert
1047        let expected = fs::read_to_string("tests/data/expected_pydantic.py")
1048            .expect("Could not read expected file");
1049        assert_eq!(rendered, expected);
1050    }
1051
1052    #[test]
1053    fn test_convert_to_pydantic_unitdef() {
1054        // Arrange
1055        let rendered = build_and_convert(
1056            "tests/data/model_unitdef.md",
1057            Templates::PythonPydantic,
1058            Some(&HashMap::from([(
1059                "astropy".to_string(),
1060                "true".to_string(),
1061            )])),
1062        );
1063
1064        // Assert
1065        let expected = fs::read_to_string("tests/data/expected_pydantic_unitdef.py")
1066            .expect("Could not read expected file");
1067        assert_eq!(rendered, expected);
1068    }
1069
1070    #[test]
1071    fn test_convert_to_graphql() {
1072        // Arrange
1073        let rendered = build_and_convert("tests/data/model.md", Templates::Graphql, None);
1074
1075        // Assert
1076        let expected = fs::read_to_string("tests/data/expected_graphql.graphql")
1077            .expect("Could not read expected file");
1078        assert_eq!(rendered, expected);
1079    }
1080
1081    #[test]
1082    fn test_convert_to_golang() {
1083        // Arrange
1084        let rendered = build_and_convert("tests/data/model.md", Templates::Golang, None);
1085
1086        // Assert
1087        let expected = fs::read_to_string("tests/data/expected_golang.go")
1088            .expect("Could not read expected file");
1089        assert_eq!(rendered, expected);
1090    }
1091
1092    #[test]
1093    fn test_convert_to_golang_gorm() {
1094        // Arrange
1095        let rendered = build_and_convert(
1096            "tests/data/model_golang_gorm.md",
1097            Templates::Golang,
1098            Some(&HashMap::from([("gorm".to_string(), "true".to_string())])),
1099        );
1100
1101        // Assert
1102        let expected = fs::read_to_string("tests/data/expected_golang_gorm.go")
1103            .expect("Could not read expected file");
1104        assert_eq!(rendered, expected);
1105    }
1106
1107    #[test]
1108    fn test_convert_to_rust() {
1109        // Arrange
1110        let rendered = build_and_convert("tests/data/model.md", Templates::Rust, None);
1111
1112        // Assert
1113        let expected = fs::read_to_string("tests/data/expected_rust.rs")
1114            .expect("Could not read expected file");
1115        assert_eq!(rendered, expected);
1116    }
1117
1118    #[test]
1119    fn test_convert_to_rust_forbidden_names() {
1120        // Arrange
1121        let rendered =
1122            build_and_convert("tests/data/model_forbidden_names.md", Templates::Rust, None);
1123
1124        // Assert
1125        let expected = fs::read_to_string("tests/data/expected_rust_forbidden.rs")
1126            .expect("Could not read expected file");
1127        assert_eq!(rendered, expected);
1128    }
1129
1130    #[test]
1131    fn test_convert_to_rust_ld() {
1132        // Arrange
1133        let rendered = build_and_convert(
1134            "tests/data/model.md",
1135            Templates::Rust,
1136            Some(&HashMap::from([("jsonld".to_string(), "true".to_string())])),
1137        );
1138
1139        // Assert
1140        let expected = fs::read_to_string("tests/data/expected_rust_ld.rs")
1141            .expect("Could not read expected file");
1142        assert_eq!(rendered, expected);
1143    }
1144
1145    #[test]
1146    fn test_convert_to_protobuf() {
1147        // Arrange
1148        let rendered = build_and_convert("tests/data/model.md", Templates::Protobuf, None);
1149
1150        // Assert
1151        let expected = fs::read_to_string("tests/data/expected_protobuf.proto")
1152            .expect("Could not read expected file");
1153        assert_eq!(rendered, expected);
1154    }
1155
1156    #[test]
1157    fn test_convert_to_mermaid() {
1158        // Arrange
1159        let rendered = build_and_convert("tests/data/model.md", Templates::Mermaid, None);
1160
1161        // Assert
1162        let expected = fs::read_to_string("tests/data/expected_mermaid.md")
1163            .expect("Could not read expected file");
1164        assert_eq!(rendered, expected);
1165    }
1166}