Skip to main content

mdmodels_core/
validation.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 crate::{
25    attribute::Attribute,
26    datamodel::DataModel,
27    markdown::{frontmatter::FrontMatter, position::Position},
28    object::{Enumeration, Object},
29    xmltype::XMLType,
30};
31use colored::Colorize;
32use log::error;
33use serde::{Deserialize, Serialize};
34use std::collections::{HashMap, HashSet};
35use std::error::Error;
36use std::fmt::{Display, Formatter};
37
38#[cfg(feature = "wasm")]
39use tsify_next::Tsify;
40
41// Basic types that are ignored in the validation process
42pub(crate) const BASIC_TYPES: [&str; 7] = [
43    "string", "number", "integer", "boolean", "float", "date", "bytes",
44];
45
46/// Represents a validation error in the data model.
47#[derive(Debug, Clone, Serialize, PartialEq)]
48#[cfg_attr(feature = "wasm", derive(Tsify))]
49#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
50pub struct ValidationError {
51    pub message: String,
52    pub object: Option<String>,
53    pub attribute: Option<String>,
54    pub location: String,
55    pub solution: Option<String>,
56    pub error_type: ErrorType,
57    pub positions: Vec<Position>,
58}
59
60impl Display for ValidationError {
61    /// Formats the validation error for display.
62    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63        let lines: Vec<String> = self.positions.iter().map(|p| p.line.to_string()).collect();
64        let mut line = lines.join(", ");
65
66        if !lines.is_empty() {
67            line = format!("[line: {line}]");
68        } else {
69            line = "".to_string();
70        }
71
72        write!(
73            f,
74            "{}[{}{}] {}:\n\t└── {}\n\t    {}",
75            line,
76            self.object.clone().unwrap_or("Global".into()).bold(),
77            match &self.attribute {
78                Some(attr) => format!(".{attr}"),
79                None => "".into(),
80            },
81            self.error_type.to_string().bold(),
82            self.message.red().bold(),
83            self.solution.clone().unwrap_or("".into()).yellow().bold(),
84        )?;
85        Ok(())
86    }
87}
88
89/// Enum representing the type of validation error.
90#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
91#[cfg_attr(feature = "wasm", derive(Tsify))]
92#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
93pub enum ErrorType {
94    NameError,
95    TypeError,
96    DuplicateError,
97    GlobalError,
98    XMLError,
99    ObjectError,
100}
101
102impl Display for ErrorType {
103    /// Formats the error type for display.
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        match self {
106            ErrorType::NameError => write!(f, "NameError"),
107            ErrorType::TypeError => write!(f, "TypeError"),
108            ErrorType::DuplicateError => write!(f, "DuplicateError"),
109            ErrorType::GlobalError => write!(f, "GlobalError"),
110            ErrorType::XMLError => write!(f, "XMLError"),
111            ErrorType::ObjectError => write!(f, "ObjectError"),
112        }
113    }
114}
115
116/// Validator for checking the integrity of a data model.
117#[derive(Debug, Clone, Serialize, PartialEq)]
118#[cfg_attr(feature = "wasm", derive(Tsify))]
119#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
120pub struct Validator {
121    pub is_valid: bool,
122    pub errors: Vec<ValidationError>,
123    #[serde(skip_serializing)]
124    pub object_positions: HashMap<String, Vec<Position>>,
125    #[serde(skip_serializing)]
126    pub enum_positions: HashMap<String, Vec<Position>>,
127}
128
129impl Error for Validator {}
130
131impl Display for Validator {
132    /// Formats the validator for display.
133    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134        for error in &self.errors {
135            error.fmt(f)?;
136        }
137        Ok(())
138    }
139}
140
141impl Validator {
142    /// Creates a new instance of `Validator`.
143    pub fn new() -> Self {
144        Self {
145            is_valid: true,
146            errors: vec![],
147            object_positions: HashMap::new(),
148            enum_positions: HashMap::new(),
149        }
150    }
151    pub fn reset(&mut self) {
152        self.is_valid = true;
153        self.errors.clear();
154        self.object_positions.clear();
155        self.enum_positions.clear();
156    }
157
158    /// Adds a validation error to the validator.
159    ///
160    /// # Arguments
161    ///
162    /// * `error` - The validation error to be added.
163    pub fn add_error(&mut self, error: ValidationError) {
164        self.errors.push(error);
165        self.is_valid = false;
166    }
167
168    /// Prints all validation errors to the log.
169    ///
170    /// This method iterates over the `errors` vector and logs each error using the `error!` macro.
171    pub fn log_result(&self) {
172        for error in &self.errors {
173            error!("{}", error);
174        }
175    }
176
177    /// Validates the provided `DataModel`.
178    ///
179    /// # Arguments
180    ///
181    /// * `model` - A reference to the `DataModel` to be validated.
182    ///
183    /// # Returns
184    ///
185    /// A `Result` which is:
186    /// - `Ok(())` if the model is valid.
187    /// - `Err(Box<dyn Error>)` if the model is invalid.
188    pub fn validate(&mut self, model: &DataModel) {
189        // If there are errors from a previous validation, reset the validator
190        self.reset();
191
192        // Extract the positions of all objects, enums, and attributes
193        self.object_positions = extract_object_positions(model);
194        self.enum_positions = extract_enum_positions(model);
195
196        // Extract the type names from the model
197        let types = Self::extract_type_names(model);
198
199        // Check for duplicate object names
200        self.check_duplicate_objects(&model.objects);
201        self.check_duplicate_enums(&model.enums);
202        self.check_has_no_objects(model);
203
204        // Validate the objects and enums
205        for object in &model.objects {
206            self.validate_object(object, &types, &model.clone().config.unwrap_or_default());
207        }
208
209        self.sort_errors();
210    }
211
212    /// Checks for duplicate object names within the model.
213    ///
214    /// # Arguments
215    ///
216    /// * `collection` - A slice of `Object` instances to be checked.
217    fn check_duplicate_objects(&mut self, collection: &[Object]) {
218        let unique = collection
219            .iter()
220            .map(|object| object.name.as_str())
221            .collect::<Vec<&str>>();
222
223        let duplicates = unique_elements(&get_duplicates(&unique));
224
225        if !duplicates.is_empty() {
226            for name in duplicates {
227                self.add_error(ValidationError {
228                    message: format!("Object '{name}' is defined more than once."),
229                    object: Some(name.to_string()),
230                    attribute: None,
231                    location: "Global".into(),
232                    error_type: ErrorType::DuplicateError,
233                    solution: Some(format!(
234                        "Rename the object(s) at lines {} to be unique.",
235                        get_line_numbers(self.object_positions.get(name).unwrap_or(&vec![]))
236                    )),
237                    positions: self
238                        .object_positions
239                        .get(name)
240                        .cloned()
241                        .unwrap_or_default()
242                        .clone(),
243                });
244            }
245        }
246    }
247
248    /// Checks for duplicate enum names within the model.
249    ///
250    /// # Arguments
251    ///
252    /// * `collection` - A slice of `Enumeration` instances to be checked.
253    fn check_duplicate_enums(&mut self, collection: &[Enumeration]) {
254        let unique = collection
255            .iter()
256            .map(|object| object.name.as_str())
257            .collect::<Vec<&str>>();
258
259        // Find duplicates
260        let duplicates = unique
261            .iter()
262            .cloned()
263            .filter(|&name| unique.iter().filter(|&n| n == &name).count() > 1)
264            .collect::<Vec<&str>>();
265
266        let duplicates = unique_elements(&duplicates);
267
268        if !duplicates.is_empty() {
269            for name in duplicates {
270                self.add_error(ValidationError {
271                    message: format!("Enumeration '{name}' is defined more than once."),
272                    object: Some(name.to_string()),
273                    attribute: None,
274                    location: "Global".into(),
275                    error_type: ErrorType::DuplicateError,
276                    solution: Some(format!(
277                        "Rename the enumeration(s) at lines {} to be unique.",
278                        get_line_numbers(self.enum_positions.get(name).unwrap_or(&vec![]))
279                    )),
280                    positions: self.enum_positions.get(name).cloned().unwrap_or_default(),
281                });
282            }
283        }
284    }
285
286    /// Validates a single object within the data model.
287    ///
288    /// # Arguments
289    ///
290    /// * `object` - A reference to the `Object` to be validated.
291    /// * `types` - A slice of type names that are valid within the model.
292    /// * `frontmatter` - A reference to the `FrontMatter` to be validated.
293    fn validate_object(&mut self, object: &Object, types: &[&str], frontmatter: &FrontMatter) {
294        self.validate_object_name(&object.name);
295
296        if !frontmatter.allow_empty {
297            self.check_has_attributes(object);
298        }
299
300        self.check_duplicate_attributes(object);
301
302        // Validate the attributes of the object
303        object.attributes.iter().for_each(|attribute| {
304            self.validate_attribute(attribute, types, object);
305        });
306    }
307
308    /// Checks for duplicate attribute names within an object.
309    ///
310    /// # Arguments
311    ///
312    /// * `object` - A reference to the `Object` to be checked.
313    fn check_duplicate_attributes(&mut self, object: &Object) {
314        // Check if the object has duplicate attributes
315        let attr_names = object
316            .attributes
317            .iter()
318            .map(|attribute| attribute.name.as_str())
319            .collect::<Vec<&str>>();
320
321        let attribute_positions = extract_attribute_positions(object);
322
323        let unique = unique_elements(&attr_names);
324        if attr_names.len() != unique.len() {
325            let duplicates = unique_elements(&get_duplicates(&attr_names));
326
327            for name in duplicates {
328                self.add_error(ValidationError {
329                    message: format!("Property '{name}' is defined more than once."),
330                    object: Some(object.name.clone()),
331                    attribute: Some(name.to_string()),
332                    location: "Global".into(),
333                    error_type: ErrorType::DuplicateError,
334                    solution: Some(format!(
335                        "Rename the property(ies) at lines {} to be unique.",
336                        get_line_numbers(attribute_positions.get(name).unwrap_or(&vec![]))
337                    )),
338                    positions: attribute_positions.get(name).cloned().unwrap_or_default(),
339                });
340            }
341        }
342    }
343
344    /// Checks if an object has attributes.
345    ///
346    /// # Arguments
347    ///
348    /// * `object` - A reference to the `Object` to be checked.
349    fn check_has_attributes(&mut self, object: &Object) {
350        if !object.has_attributes() {
351            self.add_error(ValidationError {
352                message: format!("Type '{}' is empty and has no properties.", object.name),
353                object: Some(object.name.clone()),
354                attribute: None,
355                location: "Global".into(),
356                error_type: ErrorType::ObjectError,
357                solution: Some(format!("Add a property to the object '{}'.", object.name)),
358                positions: self
359                    .object_positions
360                    .get(&object.name)
361                    .cloned()
362                    .unwrap_or_default(),
363            });
364        }
365    }
366
367    /// Validates the name of an object.
368    ///
369    /// # Arguments
370    ///
371    /// * `name` - The name of the object to be validated.
372    fn validate_object_name(&mut self, name: &str) {
373        let checks = vec![starts_with_character, contains_white_space, |name: &str| {
374            contains_special_characters(name, false)
375        }];
376
377        for check in checks {
378            if let Err((e, solution)) = check(name) {
379                self.add_error(ValidationError {
380                    message: e,
381                    object: Some(name.to_string()),
382                    attribute: None,
383                    solution: Some(format!("Resolve the issue by using '{solution}'.")),
384                    location: "Global".into(),
385                    error_type: ErrorType::NameError,
386                    positions: self.object_positions.get(name).cloned().unwrap_or_default(),
387                });
388            }
389        }
390    }
391
392    /// Checks if the model has no objects.
393    ///
394    /// # Arguments
395    ///
396    /// * `model` - A reference to the `DataModel` to be checked.
397    fn check_has_no_objects(&mut self, model: &DataModel) {
398        if model.objects.is_empty() {
399            self.add_error(ValidationError {
400                message: "This model has no definitions.".into(),
401                object: Some("Model".into()),
402                attribute: None,
403                solution: Some("Add an object to the model.".into()),
404                location: "Global".into(),
405                error_type: ErrorType::GlobalError,
406                positions: vec![],
407            });
408        }
409    }
410
411    /// Validates a single attribute within an object.
412    ///
413    /// # Arguments
414    ///
415    /// * `attribute` - A reference to the `Attribute` to be validated.
416    /// * `types` - A slice of type names that are valid within the model.
417    /// * `obj_name` - The name of the object that contains the attribute.
418    fn validate_attribute(&mut self, attribute: &Attribute, types: &[&str], object: &Object) {
419        self.validate_attribute_name(&attribute.name, object);
420
421        let attribute_positions = extract_attribute_positions(object);
422
423        if attribute.dtypes.is_empty() {
424            self.add_error(ValidationError {
425                message: format!("Property '{}' has no type specified.", attribute.name),
426                object: Some(object.name.clone()),
427                attribute: Some(attribute.name.clone()),
428                location: "Global".into(),
429                error_type: ErrorType::TypeError,
430                solution: Some(format!(
431                    "Add a type to the property '{}' using '- {}: <TYPE>'.",
432                    attribute.name, attribute.name
433                )),
434                positions: attribute_positions
435                    .get(&attribute.name)
436                    .cloned()
437                    .unwrap_or_default(),
438            })
439        }
440
441        for dtype in &attribute.dtypes {
442            self.check_attr_dtype(attribute, types, object, dtype);
443        }
444
445        if let Some(xml_option) = &attribute.xml {
446            match xml_option {
447                XMLType::Attribute { name, .. } => {
448                    self.validate_xml_attribute_option(name, &object.name, &attribute.name);
449                }
450                XMLType::Element { name, .. } => {
451                    self.validate_xml_element_option(name, &object.name, &attribute.name);
452                }
453                XMLType::Wrapped { name, wrapped, .. } => {
454                    self.validate_xml_wrapped_option(name, &object.name, &attribute.name, wrapped);
455                }
456            }
457        }
458    }
459
460    /// Checks the data type of attribute.
461    ///
462    /// # Arguments
463    ///
464    /// * `attribute` - A reference to the `Attribute` to be checked.
465    /// * `types` - A slice of type names that are valid within the model.
466    /// * `obj_name` - The name of the object that contains the attribute.
467    /// * `dtype` - The data type of the attribute to be checked.
468    fn check_attr_dtype(
469        &mut self,
470        attribute: &Attribute,
471        types: &[&str],
472        object: &Object,
473        dtype: &str,
474    ) {
475        let attribute_positions = extract_attribute_positions(object);
476
477        if dtype.is_empty() {
478            self.add_error(ValidationError {
479                message: format!(
480                    "Property '{}' has no type defined. Either define a type or use a base type.",
481                    attribute.name
482                ),
483                object: Some(object.name.clone()),
484                attribute: Some(attribute.name.clone()),
485                location: "Global".into(),
486                error_type: ErrorType::TypeError,
487                solution: Some(format!(
488                    "Add a type to the property '{}' using '- {}: TYPE' after the property name.",
489                    attribute.name, attribute.name
490                )),
491                positions: attribute_positions
492                    .get(&attribute.name)
493                    .cloned()
494                    .unwrap_or_default(),
495            });
496
497            return;
498        }
499
500        if !types.contains(&dtype) && !BASIC_TYPES.contains(&dtype) {
501            self.add_error(ValidationError {
502                message: format!(
503                    "Type '{}' of property '{}' not found.",
504                    dtype, attribute.name
505                ),
506                object: Some(object.name.clone()),
507                attribute: Some(attribute.name.clone()),
508                location: "Global".into(),
509                error_type: ErrorType::TypeError,
510                solution: Some(format!(
511                    "Add the type '{dtype}' to the model or use a base type."
512                )),
513                positions: attribute_positions
514                    .get(&attribute.name)
515                    .cloned()
516                    .unwrap_or_default(),
517            })
518        }
519    }
520
521    /// Validates the name of an attribute.
522    ///
523    /// # Arguments
524    ///
525    /// * `name` - The name of the attribute to be validated.
526    /// * `obj_name` - The name of the object that contains the attribute.
527    fn validate_attribute_name(&mut self, name: &str, object: &Object) {
528        let checks = vec![starts_with_character, contains_white_space, |name: &str| {
529            contains_special_characters(name, false)
530        }];
531
532        let attribute_positions = extract_attribute_positions(object);
533
534        for check in checks {
535            if let Err((e, solution)) = check(name) {
536                self.add_error(ValidationError {
537                    message: e,
538                    object: Some(object.name.clone()),
539                    attribute: Some(name.to_string()),
540                    location: "Global".into(),
541                    error_type: ErrorType::NameError,
542                    solution: Some(format!("Resolve the issue by using '{solution}'.")),
543                    positions: attribute_positions.get(name).cloned().unwrap_or_default(),
544                });
545            }
546        }
547    }
548
549    /// Validates an XML element option string.
550    ///
551    /// # Arguments
552    ///
553    /// * `option` - The XML element option string to validate. Can contain multiple comma-separated values.
554    ///
555    /// Checks that:
556    /// - The option string is not empty
557    /// - Each comma-separated value contains no special characters
558    fn validate_xml_element_option(
559        &mut self,
560        option: &str,
561        object_name: &str,
562        attribute_name: &str,
563    ) {
564        let option = option.trim();
565        if option.is_empty() {
566            self.add_error(ValidationError {
567                message: "XML option is not defined.".into(),
568                object: Some(object_name.to_string()),
569                attribute: Some(attribute_name.to_string()),
570                location: "Global".into(),
571                error_type: ErrorType::XMLError,
572                solution: Some(format!(
573                    "Add an XML option to the property '{attribute_name}' using '- XML: <TAG_NAME>' in a sub-list below the property name."
574                )),
575                positions: vec![],
576            });
577        }
578
579        let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
580        for opt in options {
581            if let Err((e, solution)) = contains_special_characters(opt.trim(), false) {
582                self.add_error(ValidationError {
583                    message: e,
584                    object: Some(object_name.to_string()),
585                    attribute: Some(attribute_name.to_string()),
586                    location: "Global".into(),
587                    error_type: ErrorType::XMLError,
588                    solution: Some(format!("Resolve the issue by using '{solution}'.")),
589                    positions: vec![],
590                });
591            }
592        }
593    }
594
595    /// Validates a wrapped XML element option string.
596    ///
597    /// # Arguments
598    ///
599    /// * `option` - The XML element option string to validate. Can contain multiple comma-separated values.
600    ///
601    /// Checks that:
602    /// - The option string is not empty
603    /// - Each comma-separated value contains no special characters
604    fn validate_xml_wrapped_option(
605        &mut self,
606        option: &str,
607        object_name: &str,
608        attribute_name: &str,
609        wrapped: &Option<Vec<String>>,
610    ) {
611        let option = option.trim();
612        if option.is_empty() {
613            self.add_error(ValidationError {
614                message: "XML option is not defined.".into(),
615                object: Some(object_name.to_string()),
616                attribute: Some(attribute_name.to_string()),
617                solution: Some(format!(
618                    "Add an XML option to the property '{attribute_name}' using '- XML: <TAG_NAME>' in a sub-list below the property name."
619                )),
620                location: "Global".into(),
621                error_type: ErrorType::XMLError,
622                positions: vec![],
623            });
624        }
625
626        if let Some(wrapped_types) = wrapped {
627            if wrapped_types.len() > 2 {
628                self.add_error(ValidationError {
629                    message: "XML wrapped option can only contain two types.".into(),
630                    object: Some(object_name.to_string()),
631                    attribute: Some(attribute_name.to_string()),
632                    solution: Some("Reduce the depth of the wrapped option to two types and create a new object for the third type.".to_string()),
633                    location: "Global".into(),
634                    error_type: ErrorType::XMLError,
635                    positions: vec![],
636                });
637            }
638
639            wrapped_types.iter().for_each(|wrapped_type| {
640                if let Err((e, solution)) = contains_special_characters(wrapped_type, true) {
641                    self.add_error(ValidationError {
642                        message: e,
643                        object: Some(object_name.to_string()),
644                        attribute: Some(attribute_name.to_string()),
645                        solution: Some(format!("Resolve the issue by using '{solution}'.")),
646                        location: "Global".into(),
647                        error_type: ErrorType::XMLError,
648                        positions: vec![],
649                    });
650                }
651            });
652        }
653
654        let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
655        for opt in options {
656            if let Err((e, solution)) = contains_special_characters(opt.trim(), false) {
657                self.add_error(ValidationError {
658                    message: e,
659                    object: Some(object_name.to_string()),
660                    attribute: Some(attribute_name.to_string()),
661                    solution: Some(format!("Resolve the issue by using '{solution}'.")),
662                    location: "Global".into(),
663                    error_type: ErrorType::XMLError,
664                    positions: vec![],
665                });
666            }
667        }
668    }
669
670    /// Validates an XML attribute option string.
671    ///
672    /// # Arguments
673    ///
674    /// * `option` - The XML attribute option string to validate. Can contain multiple comma-separated values.
675    /// * `object_name` - The name of the object containing this attribute
676    /// * `attribute_name` - The name of the attribute being validated
677    ///
678    /// Checks that:
679    /// - The option string is not empty
680    /// - Each comma-separated value contains no special characters
681    ///
682    /// # Errors
683    ///
684    /// Adds validation errors to the validator if:
685    /// - The option string is empty
686    /// - Any of the comma-separated values contain special characters
687    fn validate_xml_attribute_option(
688        &mut self,
689        option: &str,
690        object_name: &str,
691        attribute_name: &str,
692    ) {
693        let option = option.trim();
694        if option.is_empty() {
695            self.add_error(ValidationError {
696                message: "XML attribute option is not defined.".into(),
697                object: Some(object_name.to_string()),
698                attribute: Some(attribute_name.to_string()),
699                solution: Some(format!(
700                    "Add an XML option to the property '{attribute_name}' using '- XML: @<ATTRIBUTE_NAME>' in a sub-list below the property name."
701                )),
702                location: "Global".into(),
703                error_type: ErrorType::XMLError,
704                positions: vec![],
705            });
706        }
707
708        let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
709        for opt in options {
710            if let Err((e, solution)) = contains_special_characters(opt, false) {
711                self.add_error(ValidationError {
712                    message: e,
713                    object: Some(object_name.to_string()),
714                    attribute: Some(attribute_name.to_string()),
715                    solution: Some(format!("Resolve the issue by using '{solution}'.")),
716                    location: "Global".into(),
717                    error_type: ErrorType::XMLError,
718                    positions: vec![],
719                });
720            }
721        }
722    }
723
724    /// Extracts the type names from the data model.
725    ///
726    /// # Arguments
727    ///
728    /// * `model` - A reference to the `DataModel` to extract type names from.
729    ///
730    /// # Returns
731    ///
732    /// A vector of type names.
733    fn extract_type_names(model: &DataModel) -> Vec<&str> {
734        let types = model
735            .objects
736            .iter()
737            .map(|object| object.name.as_str())
738            .chain(model.enums.iter().map(|enum_| enum_.name.as_str()))
739            .collect::<Vec<&str>>();
740        types
741    }
742
743    /// Sorts the validation errors by their line number, allowing for easier identification
744    /// of issues in the source code. The sorting is done in-place on the `errors` vector.
745    fn sort_errors(&mut self) {
746        self.errors.sort_by(|a, b| {
747            let line_a = a.positions.first().map(|pos| pos.line);
748            let line_b = b.positions.first().map(|pos| pos.line);
749            line_a.cmp(&line_b)
750        });
751    }
752}
753
754impl Default for Validator {
755    /// Provides a default implementation for `Validator`.
756    fn default() -> Self {
757        Self::new()
758    }
759}
760
761/// Returns a list of unique elements from a slice.
762///
763/// # Arguments
764///
765/// * `input` - A slice of elements to be checked for uniqueness.
766///
767/// # Returns
768///
769/// A vector of unique elements.
770fn unique_elements<T: Eq + std::hash::Hash + Clone>(input: &[T]) -> Vec<T> {
771    let mut set = HashSet::new();
772
773    for item in input {
774        set.insert(item.clone());
775    }
776
777    set.into_iter().collect()
778}
779
780/// Returns a list of duplicate elements from a slice.
781///
782/// # Arguments
783///
784/// * `collection` - A slice of elements to be checked for duplicates.
785///
786/// # Returns
787///
788/// A vector of duplicate elements.
789fn get_duplicates<'a>(collection: &'a [&'a str]) -> Vec<&'a str> {
790    let mut seen = HashSet::new();
791    let mut duplicates = HashSet::new();
792
793    for &item in collection {
794        if !seen.insert(item) {
795            duplicates.insert(item);
796        }
797    }
798
799    duplicates.into_iter().collect()
800}
801
802/// Checks if the given name starts with an alphabetic character.
803///
804/// # Arguments
805///
806/// * `name` - A string slice that holds the name to be checked.
807///
808/// # Returns
809///
810/// A `Result` which is:
811/// - `Ok(())` if the name starts with an alphabetic character.
812/// - `Err((String, String))` if the name does not start with an alphabetic character.
813fn starts_with_character(name: &str) -> Result<(), (String, String)> {
814    match name.chars().next() {
815        Some(c) if c.is_alphabetic() => Ok(()),
816        _ => Err((
817            format!("Name '{name}' must start with a letter."),
818            name[1..].to_string(),
819        )),
820    }
821}
822
823/// Checks if the given name contains whitespace.
824///
825/// # Arguments
826///
827/// * `name` - A string slice that holds the name to be checked.
828///
829/// # Returns
830///
831/// A `Result` which is:
832/// - `Ok(())` if the name does not contain whitespace.
833/// - `Err(String)` if the name contains whitespace.
834fn contains_white_space(name: &str) -> Result<(), (String, String)> {
835    if name.contains(' ') {
836        Err((
837            format!(
838                "Name '{name}' contains whitespace, which is not valid. Use underscores instead."
839            ),
840            name.replace(" ", "_").to_string(),
841        ))
842    } else {
843        Ok(())
844    }
845}
846
847/// Checks if the given name contains special characters, except for underscores.
848///
849/// # Arguments
850///
851/// * `name` - A string slice that holds the name to be checked.
852///
853/// # Returns
854///
855/// A `Result` which is:
856/// - `Ok(())` if the name does not contain special characters.
857/// - `Err(String)` if the name contains special characters.
858fn contains_special_characters(name: &str, allow_slash: bool) -> Result<(), (String, String)> {
859    if name
860        .chars()
861        .any(|c| !c.is_alphanumeric() && c != '_' && c != ' ' && (!allow_slash || c != '/'))
862    {
863        Err((
864        format!("Name '{name}' contains special characters, which are not valid except for underscores."),
865            name.chars().filter(|c| c.is_alphanumeric() || *c == '_').collect::<String>().to_string(),
866        ))
867    } else {
868        Ok(())
869    }
870}
871
872/// Extracts the positions of all objects in the data model.
873///
874/// # Arguments
875///
876/// * `model` - A reference to the `DataModel` to extract positions from.
877///
878/// # Returns
879///
880/// A `HashMap` mapping object names to their positions in the source code.
881fn extract_object_positions(model: &DataModel) -> HashMap<String, Vec<Position>> {
882    let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
883    for object in &model.objects {
884        if object.position.is_none() {
885            continue;
886        }
887
888        if let Some(pos) = positions.get_mut(&object.name) {
889            pos.push(object.position.unwrap());
890        } else {
891            positions.insert(object.name.clone(), vec![object.position.unwrap()]);
892        }
893    }
894    positions
895}
896
897/// Extracts the positions of all enums in the data model.
898///
899/// # Arguments
900///
901/// * `model` - A reference to the `DataModel` to extract positions from.
902///
903/// # Returns
904///
905/// A `HashMap` mapping enum names to their positions in the source code.
906fn extract_enum_positions(model: &DataModel) -> HashMap<String, Vec<Position>> {
907    let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
908    for enum_ in &model.enums {
909        if enum_.position.is_none() {
910            continue;
911        }
912
913        if let Some(pos) = positions.get_mut(&enum_.name) {
914            pos.push(enum_.position.unwrap());
915        } else {
916            positions.insert(enum_.name.clone(), vec![enum_.position.unwrap()]);
917        }
918    }
919    positions
920}
921
922/// Extracts the positions of all attributes across all objects in the data model.
923///
924/// # Arguments
925///
926/// * `model` - A reference to the `DataModel` to extract positions from.
927///
928/// # Returns
929///
930/// A `HashMap` mapping attribute names to their positions in the source code.
931fn extract_attribute_positions(object: &Object) -> HashMap<String, Vec<Position>> {
932    let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
933    for attribute in &object.attributes {
934        if attribute.position.is_none() {
935            continue;
936        }
937
938        if let Some(pos) = positions.get_mut(&attribute.name) {
939            pos.push(attribute.position.unwrap());
940        } else {
941            positions.insert(attribute.name.clone(), vec![attribute.position.unwrap()]);
942        }
943    }
944    positions
945}
946
947/// Extracts line numbers from a slice of Position objects.
948///
949/// # Arguments
950///
951/// * `positions` - A slice of Position objects containing line number information.
952///
953/// # Returns
954///
955/// A string containing the line numbers.
956fn get_line_numbers(positions: &[Position]) -> String {
957    positions
958        .iter()
959        .map(|p| p.line.to_string())
960        .collect::<Vec<String>>()
961        .join(", ")
962}