1use 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
41pub(crate) const BASIC_TYPES: [&str; 7] = [
43 "string", "number", "integer", "boolean", "float", "date", "bytes",
44];
45
46#[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 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#[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 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#[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 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 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 pub fn add_error(&mut self, error: ValidationError) {
164 self.errors.push(error);
165 self.is_valid = false;
166 }
167
168 pub fn log_result(&self) {
172 for error in &self.errors {
173 error!("{}", error);
174 }
175 }
176
177 pub fn validate(&mut self, model: &DataModel) {
189 self.reset();
191
192 self.object_positions = extract_object_positions(model);
194 self.enum_positions = extract_enum_positions(model);
195
196 let types = Self::extract_type_names(model);
198
199 self.check_duplicate_objects(&model.objects);
201 self.check_duplicate_enums(&model.enums);
202 self.check_has_no_objects(model);
203
204 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 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 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 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 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 object.attributes.iter().for_each(|attribute| {
304 self.validate_attribute(attribute, types, object);
305 });
306 }
307
308 fn check_duplicate_attributes(&mut self, object: &Object) {
314 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 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 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, 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 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 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 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 fn validate_attribute_name(&mut self, name: &str, object: &Object) {
528 let checks = vec![starts_with_character, contains_white_space, |name: &str| {
532 contains_special_characters(name, false, true)
533 }];
534
535 let attribute_positions = extract_attribute_positions(object);
536
537 for check in checks {
538 if let Err((e, solution)) = check(name) {
539 self.add_error(ValidationError {
540 message: e,
541 object: Some(object.name.clone()),
542 attribute: Some(name.to_string()),
543 location: "Global".into(),
544 error_type: ErrorType::NameError,
545 solution: Some(format!("Resolve the issue by using '{solution}'.")),
546 positions: attribute_positions.get(name).cloned().unwrap_or_default(),
547 });
548 }
549 }
550 }
551
552 fn validate_xml_element_option(
562 &mut self,
563 option: &str,
564 object_name: &str,
565 attribute_name: &str,
566 ) {
567 let option = option.trim();
568 if option.is_empty() {
569 self.add_error(ValidationError {
570 message: "XML option is not defined.".into(),
571 object: Some(object_name.to_string()),
572 attribute: Some(attribute_name.to_string()),
573 location: "Global".into(),
574 error_type: ErrorType::XMLError,
575 solution: Some(format!(
576 "Add an XML option to the property '{attribute_name}' using '- XML: <TAG_NAME>' in a sub-list below the property name."
577 )),
578 positions: vec![],
579 });
580 }
581
582 let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
583 for opt in options {
584 if let Err((e, solution)) = contains_special_characters(opt.trim(), false, true) {
585 self.add_error(ValidationError {
586 message: e,
587 object: Some(object_name.to_string()),
588 attribute: Some(attribute_name.to_string()),
589 location: "Global".into(),
590 error_type: ErrorType::XMLError,
591 solution: Some(format!("Resolve the issue by using '{solution}'.")),
592 positions: vec![],
593 });
594 }
595 }
596 }
597
598 fn validate_xml_wrapped_option(
608 &mut self,
609 option: &str,
610 object_name: &str,
611 attribute_name: &str,
612 wrapped: &Option<Vec<String>>,
613 ) {
614 let option = option.trim();
615 if option.is_empty() {
616 self.add_error(ValidationError {
617 message: "XML option is not defined.".into(),
618 object: Some(object_name.to_string()),
619 attribute: Some(attribute_name.to_string()),
620 solution: Some(format!(
621 "Add an XML option to the property '{attribute_name}' using '- XML: <TAG_NAME>' in a sub-list below the property name."
622 )),
623 location: "Global".into(),
624 error_type: ErrorType::XMLError,
625 positions: vec![],
626 });
627 }
628
629 if let Some(wrapped_types) = wrapped {
630 if wrapped_types.len() > 2 {
631 self.add_error(ValidationError {
632 message: "XML wrapped option can only contain two types.".into(),
633 object: Some(object_name.to_string()),
634 attribute: Some(attribute_name.to_string()),
635 solution: Some("Reduce the depth of the wrapped option to two types and create a new object for the third type.".to_string()),
636 location: "Global".into(),
637 error_type: ErrorType::XMLError,
638 positions: vec![],
639 });
640 }
641
642 wrapped_types.iter().for_each(|wrapped_type| {
643 if let Err((e, solution)) = contains_special_characters(wrapped_type, true, true) {
644 self.add_error(ValidationError {
645 message: e,
646 object: Some(object_name.to_string()),
647 attribute: Some(attribute_name.to_string()),
648 solution: Some(format!("Resolve the issue by using '{solution}'.")),
649 location: "Global".into(),
650 error_type: ErrorType::XMLError,
651 positions: vec![],
652 });
653 }
654 });
655 }
656
657 let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
658 for opt in options {
659 if let Err((e, solution)) = contains_special_characters(opt.trim(), false, true) {
660 self.add_error(ValidationError {
661 message: e,
662 object: Some(object_name.to_string()),
663 attribute: Some(attribute_name.to_string()),
664 solution: Some(format!("Resolve the issue by using '{solution}'.")),
665 location: "Global".into(),
666 error_type: ErrorType::XMLError,
667 positions: vec![],
668 });
669 }
670 }
671 }
672
673 fn validate_xml_attribute_option(
691 &mut self,
692 option: &str,
693 object_name: &str,
694 attribute_name: &str,
695 ) {
696 let option = option.trim();
697 if option.is_empty() {
698 self.add_error(ValidationError {
699 message: "XML attribute option is not defined.".into(),
700 object: Some(object_name.to_string()),
701 attribute: Some(attribute_name.to_string()),
702 solution: Some(format!(
703 "Add an XML option to the property '{attribute_name}' using '- XML: @<ATTRIBUTE_NAME>' in a sub-list below the property name."
704 )),
705 location: "Global".into(),
706 error_type: ErrorType::XMLError,
707 positions: vec![],
708 });
709 }
710
711 let options = option.split(',').map(|s| s.trim()).collect::<Vec<_>>();
712 for opt in options {
713 if let Err((e, solution)) = contains_special_characters(opt, false, true) {
714 self.add_error(ValidationError {
715 message: e,
716 object: Some(object_name.to_string()),
717 attribute: Some(attribute_name.to_string()),
718 solution: Some(format!("Resolve the issue by using '{solution}'.")),
719 location: "Global".into(),
720 error_type: ErrorType::XMLError,
721 positions: vec![],
722 });
723 }
724 }
725 }
726
727 fn extract_type_names(model: &DataModel) -> Vec<&str> {
737 let types = model
738 .objects
739 .iter()
740 .map(|object| object.name.as_str())
741 .chain(model.enums.iter().map(|enum_| enum_.name.as_str()))
742 .collect::<Vec<&str>>();
743 types
744 }
745
746 fn sort_errors(&mut self) {
749 self.errors.sort_by(|a, b| {
750 let line_a = a.positions.first().map(|pos| pos.line);
751 let line_b = b.positions.first().map(|pos| pos.line);
752 line_a.cmp(&line_b)
753 });
754 }
755}
756
757impl Default for Validator {
758 fn default() -> Self {
760 Self::new()
761 }
762}
763
764fn unique_elements<T: Eq + std::hash::Hash + Clone>(input: &[T]) -> Vec<T> {
774 let mut set = HashSet::new();
775
776 for item in input {
777 set.insert(item.clone());
778 }
779
780 set.into_iter().collect()
781}
782
783fn get_duplicates<'a>(collection: &'a [&'a str]) -> Vec<&'a str> {
793 let mut seen = HashSet::new();
794 let mut duplicates = HashSet::new();
795
796 for &item in collection {
797 if !seen.insert(item) {
798 duplicates.insert(item);
799 }
800 }
801
802 duplicates.into_iter().collect()
803}
804
805fn starts_with_character(name: &str) -> Result<(), (String, String)> {
817 match name.chars().next() {
818 Some(c) if c.is_alphabetic() => Ok(()),
819 _ => Err((
820 format!("Name '{name}' must start with a letter."),
821 name[1..].to_string(),
822 )),
823 }
824}
825
826fn contains_white_space(name: &str) -> Result<(), (String, String)> {
838 if name.contains(' ') {
839 Err((
840 format!(
841 "Name '{name}' contains whitespace, which is not valid. Use underscores instead."
842 ),
843 name.replace(" ", "_").to_string(),
844 ))
845 } else {
846 Ok(())
847 }
848}
849
850fn contains_special_characters(
862 name: &str,
863 allow_slash: bool,
864 allow_dash: bool,
865) -> Result<(), (String, String)> {
866 if name.chars().any(|c| {
867 !c.is_alphanumeric()
868 && c != '_'
869 && c != ' '
870 && (!allow_slash || c != '/')
871 && (!allow_dash || c != '-')
872 }) {
873 Err((
874 format!("Name '{name}' contains special characters, which are not valid except for underscores."),
875 name.chars().filter(|c| c.is_alphanumeric() || *c == '_').collect::<String>().to_string(),
876 ))
877 } else {
878 Ok(())
879 }
880}
881
882fn extract_object_positions(model: &DataModel) -> HashMap<String, Vec<Position>> {
892 let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
893 for object in &model.objects {
894 if object.position.is_none() {
895 continue;
896 }
897
898 if let Some(pos) = positions.get_mut(&object.name) {
899 pos.push(object.position.unwrap());
900 } else {
901 positions.insert(object.name.clone(), vec![object.position.unwrap()]);
902 }
903 }
904 positions
905}
906
907fn extract_enum_positions(model: &DataModel) -> HashMap<String, Vec<Position>> {
917 let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
918 for enum_ in &model.enums {
919 if enum_.position.is_none() {
920 continue;
921 }
922
923 if let Some(pos) = positions.get_mut(&enum_.name) {
924 pos.push(enum_.position.unwrap());
925 } else {
926 positions.insert(enum_.name.clone(), vec![enum_.position.unwrap()]);
927 }
928 }
929 positions
930}
931
932fn extract_attribute_positions(object: &Object) -> HashMap<String, Vec<Position>> {
942 let mut positions: HashMap<String, Vec<Position>> = HashMap::new();
943 for attribute in &object.attributes {
944 if attribute.position.is_none() {
945 continue;
946 }
947
948 if let Some(pos) = positions.get_mut(&attribute.name) {
949 pos.push(attribute.position.unwrap());
950 } else {
951 positions.insert(attribute.name.clone(), vec![attribute.position.unwrap()]);
952 }
953 }
954 positions
955}
956
957fn get_line_numbers(positions: &[Position]) -> String {
967 positions
968 .iter()
969 .map(|p| p.line.to_string())
970 .collect::<Vec<String>>()
971 .join(", ")
972}
973
974#[cfg(test)]
975mod tests {
976 use crate::markdown::parser::parse_markdown;
977
978 #[test]
982 fn test_dashed_attribute_name_is_valid() {
983 let content = "### Configuration\n\n- coupling-scheme\n - Type: string\n";
984 let result = parse_markdown(content, None);
985 assert!(
986 result.is_ok(),
987 "dashed attribute names should pass validation, got: {:?}",
988 result.err()
989 );
990 }
991
992 #[test]
996 fn test_dashed_attribute_name_is_preserved() {
997 let content = "### Configuration\n\n- coupling-scheme\n - Type: string\n";
998 let model = parse_markdown(content, None).expect("model should parse");
999 let attr = &model.objects[0].attributes[0];
1000 assert_eq!(attr.name, "coupling-scheme");
1001 }
1002
1003 #[test]
1006 fn test_dashed_object_name_is_invalid() {
1007 let mut model = parse_markdown("### Configuration\n\n- name\n - Type: string\n", None)
1008 .expect("model should parse");
1009 model.objects[0].name = "coupling-scheme".to_string();
1010
1011 let mut validator = super::Validator::new();
1012 validator.validate(&model);
1013
1014 assert!(!validator.is_valid);
1015 assert!(validator
1016 .errors
1017 .iter()
1018 .any(|e| e.error_type == super::ErrorType::NameError
1019 && e.object.as_deref() == Some("coupling-scheme")));
1020 }
1021}