1use std::{collections::HashMap, error::Error, fmt, str::FromStr};
4
5use crate::{
6 AnyUri, DataTypeName, DateTime, Document, Element, Guid, MultiLanguageText, Reference,
7 ValueError, DRAFT_PLACEHOLDER_NAMESPACE, NAMESPACE,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ElementKind {
13 Library,
14 Subject,
15 DataTemplate,
16 ObjectType,
17 GroupOfProperties,
18 Property,
19 Unit,
20 Dimension,
21 QuantityKind,
22 ReferenceDocument,
23}
24
25impl ElementKind {
26 #[must_use]
28 pub const fn is_global_root(self) -> bool {
29 matches!(
30 self,
31 Self::Library
32 | Self::DataTemplate
33 | Self::ObjectType
34 | Self::GroupOfProperties
35 | Self::Property
36 )
37 }
38
39 #[must_use]
40 pub fn from_element(element: &Element) -> Option<Self> {
41 if element.namespace_uri() != Some(NAMESPACE) {
42 return None;
43 }
44 match element.local_name() {
45 "Library" => Some(Self::Library),
46 "Subject" => Some(Self::Subject),
47 "DataTemplate" => Some(Self::DataTemplate),
48 "ObjectType" => Some(Self::ObjectType),
49 "GroupOfProperties" => Some(Self::GroupOfProperties),
50 "Property" => Some(Self::Property),
51 "Unit" => Some(Self::Unit),
52 "Dimension" => Some(Self::Dimension),
53 "QuantityKind" => Some(Self::QuantityKind),
54 "ReferenceDocument" => Some(Self::ReferenceDocument),
55 _ => None,
56 }
57 }
58}
59
60impl Document {
61 #[must_use]
62 pub fn root_kind(&self) -> Option<ElementKind> {
63 ElementKind::from_element(self.root()).filter(|kind| kind.is_global_root())
64 }
65
66 #[must_use]
67 pub fn library(&self) -> Option<Library<'_>> {
68 (self.root_kind() == Some(ElementKind::Library)).then(|| Library {
69 element: self.root(),
70 })
71 }
72
73 #[must_use]
75 pub fn into_root_element(self) -> Element {
76 self.take_root()
77 }
78}
79
80#[derive(Debug, Clone, Copy)]
82pub struct Library<'a> {
83 element: &'a Element,
84}
85
86impl<'a> Library<'a> {
87 #[must_use]
88 pub const fn element(self) -> &'a Element {
89 self.element
90 }
91
92 pub fn guid(self) -> Result<Guid, ValueError> {
93 required_guid(self.element)
94 }
95
96 pub fn items(self) -> impl Iterator<Item = LibraryItem<'a>> {
97 self.element.children().map(classify_library_item)
98 }
99}
100
101#[derive(Debug, Clone, Copy)]
103pub enum LibraryItem<'a> {
104 Name(MultilingualTextRef<'a>),
105 DataTemplate(DataTemplateRef<'a>),
106 ObjectType(ConceptRef<'a>),
107 GroupOfProperties(ConceptRef<'a>),
108 Property(PropertyRef<'a>),
109 Unit(ConceptRef<'a>),
110 Dimension(ConceptRef<'a>),
111 QuantityKind(ConceptRef<'a>),
112 ReferenceDocument(ConceptRef<'a>),
113 Extension(&'a Element),
114}
115
116fn classify_library_item(element: &Element) -> LibraryItem<'_> {
117 if is_dt(element, "Name") {
118 return LibraryItem::Name(MultilingualTextRef { element });
119 }
120 match ElementKind::from_element(element) {
121 Some(ElementKind::DataTemplate) => LibraryItem::DataTemplate(DataTemplateRef { element }),
122 Some(ElementKind::ObjectType) => LibraryItem::ObjectType(ConceptRef { element }),
123 Some(ElementKind::GroupOfProperties) => {
124 LibraryItem::GroupOfProperties(ConceptRef { element })
125 }
126 Some(ElementKind::Property) => LibraryItem::Property(PropertyRef { element }),
127 Some(ElementKind::Unit) => LibraryItem::Unit(ConceptRef { element }),
128 Some(ElementKind::Dimension) => LibraryItem::Dimension(ConceptRef { element }),
129 Some(ElementKind::QuantityKind) => LibraryItem::QuantityKind(ConceptRef { element }),
130 Some(ElementKind::ReferenceDocument) => {
131 LibraryItem::ReferenceDocument(ConceptRef { element })
132 }
133 _ => LibraryItem::Extension(element),
134 }
135}
136
137#[derive(Debug, Clone, Copy)]
139pub struct MultilingualTextRef<'a> {
140 element: &'a Element,
141}
142
143impl<'a> MultilingualTextRef<'a> {
144 #[must_use]
145 pub fn language(self) -> Option<&'a str> {
146 self.element.attribute_ns(None, "language")
147 }
148
149 #[must_use]
150 pub fn text(self) -> String {
151 self.element.direct_text()
152 }
153
154 pub fn to_owned(self) -> Result<MultiLanguageText, ValueError> {
155 MultiLanguageText::new(self.language().unwrap_or_default(), self.text())
156 }
157}
158
159#[derive(Debug, Clone, Copy)]
161pub struct ReferenceRef<'a> {
162 element: &'a Element,
163}
164
165impl<'a> ReferenceRef<'a> {
166 pub fn guid(self) -> Option<Result<Guid, ValueError>> {
167 self.element
168 .attribute_ns(Some(NAMESPACE), "GUID")
169 .map(Guid::from_str)
170 }
171
172 #[must_use]
173 pub fn uri(self) -> Option<&'a str> {
174 self.element.attribute_ns(Some(NAMESPACE), "referenceURI")
175 }
176
177 pub fn to_owned(self) -> Result<Reference, ValueError> {
178 let guid = self.guid().transpose()?;
179 let uri = self.uri().map(str::parse).transpose()?;
180 Ok(Reference::new(guid, uri))
181 }
182}
183
184#[derive(Debug, Clone, Copy)]
186pub struct ConceptRef<'a> {
187 element: &'a Element,
188}
189
190impl<'a> ConceptRef<'a> {
191 #[must_use]
192 pub const fn element(self) -> &'a Element {
193 self.element
194 }
195
196 pub fn guid(self) -> Result<Guid, ValueError> {
197 required_guid(self.element)
198 }
199
200 #[must_use]
201 pub fn date_of_creation(self) -> Option<&'a str> {
202 self.element.attribute_ns(None, "dateOfCreation")
203 }
204
205 pub fn names(self) -> impl Iterator<Item = MultilingualTextRef<'a>> {
206 self.element
207 .children()
208 .filter(|element| is_dt(element, "Name"))
209 .map(|element| MultilingualTextRef { element })
210 }
211
212 pub fn definitions(self) -> impl Iterator<Item = MultilingualTextRef<'a>> {
213 self.element
214 .children()
215 .filter(|element| is_dt(element, "Definition"))
216 .map(|element| MultilingualTextRef { element })
217 }
218
219 pub fn references(self, local_name: &'a str) -> impl Iterator<Item = ReferenceRef<'a>> {
220 self.element
221 .children()
222 .filter(move |element| is_dt(element, local_name))
223 .map(|element| ReferenceRef { element })
224 }
225}
226
227#[derive(Debug, Clone, Copy)]
229pub struct DataTemplateRef<'a> {
230 element: &'a Element,
231}
232
233impl<'a> DataTemplateRef<'a> {
234 #[must_use]
235 pub const fn element(self) -> &'a Element {
236 self.element
237 }
238
239 #[must_use]
240 pub const fn concept(self) -> ConceptRef<'a> {
241 ConceptRef {
242 element: self.element,
243 }
244 }
245
246 pub fn property_references(self) -> impl Iterator<Item = ReferenceRef<'a>> {
247 self.concept().references("HasPropertyRef")
248 }
249
250 pub fn group_references(self) -> impl Iterator<Item = ReferenceRef<'a>> {
251 self.concept().references("HasGroupOfPropertiesRef")
252 }
253
254 pub fn object_type_reference(self) -> Option<ReferenceRef<'a>> {
255 self.concept().references("HasObjectTypeRef").next()
256 }
257}
258
259#[derive(Debug, Clone, Copy)]
261pub struct PropertyRef<'a> {
262 element: &'a Element,
263}
264
265impl<'a> PropertyRef<'a> {
266 #[must_use]
267 pub const fn element(self) -> &'a Element {
268 self.element
269 }
270
271 #[must_use]
272 pub const fn concept(self) -> ConceptRef<'a> {
273 ConceptRef {
274 element: self.element,
275 }
276 }
277
278 pub fn names(self) -> impl Iterator<Item = MultilingualTextRef<'a>> {
279 self.concept().names()
280 }
281
282 #[must_use]
283 pub fn data_type(self) -> Option<DataTypeRef<'a>> {
284 self.element
285 .children()
286 .find(|element| is_dt(element, "DataType"))
287 .map(|element| DataTypeRef { element })
288 }
289}
290
291#[derive(Debug, Clone, Copy)]
293pub struct DataTypeRef<'a> {
294 element: &'a Element,
295}
296
297impl<'a> DataTypeRef<'a> {
298 #[must_use]
299 pub fn name(self) -> Option<&'a str> {
300 self.element.attribute_ns(None, "name")
301 }
302
303 #[must_use]
304 pub fn name_kind(self) -> Option<DataTypeName> {
305 self.name().map(DataTypeName::from)
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct TypedElement<const KIND: u8> {
312 element: Element,
313}
314
315impl<const KIND: u8> TypedElement<KIND> {
316 pub fn try_from_element(element: Element) -> Result<Self, ModelError> {
317 let expected = kind_from_discriminant(KIND);
318 let actual = ElementKind::from_element(&element);
319 if actual != Some(expected) {
320 return Err(ModelError { expected, actual });
321 }
322 Ok(Self { element })
323 }
324
325 #[must_use]
326 pub const fn element(&self) -> &Element {
327 &self.element
328 }
329
330 #[must_use]
331 pub fn into_element(self) -> Element {
332 self.element
333 }
334}
335
336impl<const KIND: u8> TryFrom<Element> for TypedElement<KIND> {
337 type Error = ModelError;
338
339 fn try_from(element: Element) -> Result<Self, Self::Error> {
340 Self::try_from_element(element)
341 }
342}
343
344pub type LibraryElement = TypedElement<0>;
345pub type SubjectElement = TypedElement<1>;
346pub type DataTemplateElement = TypedElement<2>;
347pub type ObjectTypeElement = TypedElement<3>;
348pub type GroupOfPropertiesElement = TypedElement<4>;
349pub type PropertyElement = TypedElement<5>;
350pub type UnitElement = TypedElement<6>;
351pub type DimensionElement = TypedElement<7>;
352pub type QuantityKindElement = TypedElement<8>;
353pub type ReferenceDocumentElement = TypedElement<9>;
354
355const fn kind_from_discriminant(value: u8) -> ElementKind {
356 match value {
357 0 => ElementKind::Library,
358 1 => ElementKind::Subject,
359 2 => ElementKind::DataTemplate,
360 3 => ElementKind::ObjectType,
361 4 => ElementKind::GroupOfProperties,
362 5 => ElementKind::Property,
363 6 => ElementKind::Unit,
364 7 => ElementKind::Dimension,
365 8 => ElementKind::QuantityKind,
366 9 => ElementKind::ReferenceDocument,
367 _ => panic!("invalid private DT element discriminant"),
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct ModelError {
374 pub expected: ElementKind,
375 pub actual: Option<ElementKind>,
376}
377
378impl fmt::Display for ModelError {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
380 write!(
381 formatter,
382 "expected DT element {:?}, found {:?}",
383 self.expected, self.actual
384 )
385 }
386}
387
388impl Error for ModelError {}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub enum Severity {
393 Error,
394 Warning,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum DiagnosticCode {
400 DraftNamespace,
401 WrongRootNamespace,
402 UnknownRootElement,
403 MissingGuid,
404 InvalidGuid,
405 InvalidUri,
406 DuplicateGuid,
407 MissingCreationDate,
408 InvalidCreationDate,
409 MissingName,
410 MissingDefinition,
411 DuplicateDefinition,
412 EmptyReference,
413 MissingLanguage,
414 InvalidLanguage,
415 MissingDataTypeName,
416 UnknownDataType,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct Diagnostic {
422 pub severity: Severity,
423 pub code: DiagnosticCode,
424 pub path: String,
425 pub message: String,
426}
427
428impl Document {
429 #[must_use]
431 pub fn validate(&self) -> Vec<Diagnostic> {
432 let mut diagnostics = Vec::new();
433 if self.root().namespace_uri() == Some(DRAFT_PLACEHOLDER_NAMESPACE) {
434 diagnostics.push(diagnostic(
435 Severity::Error,
436 DiagnosticCode::DraftNamespace,
437 "/",
438 "root element uses the pre-release placeholder namespace, not ISO 23387 edition 2",
439 ));
440 } else if self.root().namespace_uri() != Some(NAMESPACE) {
441 diagnostics.push(diagnostic(
442 Severity::Error,
443 DiagnosticCode::WrongRootNamespace,
444 "/",
445 "root element is not in the ISO 23387 edition 2 namespace",
446 ));
447 } else if self.root_kind().is_none() {
448 diagnostics.push(diagnostic(
449 Severity::Error,
450 DiagnosticCode::UnknownRootElement,
451 "/",
452 "root element is not a recognized ISO 23387 global element",
453 ));
454 }
455
456 let mut guids = HashMap::<String, String>::new();
457 validate_element(self.root(), None, None, "", &mut guids, &mut diagnostics);
458 diagnostics
459 }
460}
461
462fn validate_element(
463 element: &Element,
464 parent_kind: Option<ElementKind>,
465 parent_local: Option<&str>,
466 parent_path: &str,
467 guids: &mut HashMap<String, String>,
468 diagnostics: &mut Vec<Diagnostic>,
469) {
470 let path = format!("{parent_path}/{}", element.qname());
471 let kind = ElementKind::from_element(element);
472 let identity = match parent_kind {
473 None => kind.is_some_and(ElementKind::is_global_root),
474 Some(ElementKind::Library) => kind.is_some(),
475 _ => false,
476 };
477 let requires_guid = identity;
478 let concept = identity && kind != Some(ElementKind::Library);
479
480 let guid = element.attribute_ns(Some(NAMESPACE), "GUID");
481 if requires_guid && guid.is_none() {
482 diagnostics.push(diagnostic(
483 Severity::Error,
484 DiagnosticCode::MissingGuid,
485 &path,
486 "required dt:GUID attribute is missing",
487 ));
488 }
489 if let Some(value) = guid {
490 if requires_guid {
491 if let Some(first_path) = guids.insert(value.to_ascii_lowercase(), path.clone()) {
492 diagnostics.push(diagnostic(
493 Severity::Error,
494 DiagnosticCode::DuplicateGuid,
495 &path,
496 format!("GUID duplicates {first_path}"),
497 ));
498 }
499 }
500 if Guid::from_str(value).is_err() {
501 diagnostics.push(diagnostic(
502 Severity::Error,
503 DiagnosticCode::InvalidGuid,
504 &path,
505 "dt:GUID does not match the ISO 23387 lexical contract",
506 ));
507 }
508 }
509 for attribute_name in ["referenceURI", "about"] {
510 if let Some(uri) = element.attribute_ns(Some(NAMESPACE), attribute_name) {
511 if uri.parse::<AnyUri>().is_err() {
512 diagnostics.push(diagnostic(
513 Severity::Error,
514 DiagnosticCode::InvalidUri,
515 &path,
516 format!("invalid dt:{attribute_name} value {uri:?}"),
517 ));
518 }
519 }
520 }
521 if concept {
522 match element.attribute_ns(None, "dateOfCreation") {
523 None => diagnostics.push(diagnostic(
524 Severity::Error,
525 DiagnosticCode::MissingCreationDate,
526 &path,
527 "ConceptType-derived element lacks dateOfCreation",
528 )),
529 Some(value) if DateTime::from_str(value).is_err() => diagnostics.push(diagnostic(
530 Severity::Error,
531 DiagnosticCode::InvalidCreationDate,
532 &path,
533 "dateOfCreation does not match the xs:dateTime lexical contract",
534 )),
535 Some(_) => {}
536 }
537 if !element.children().any(|child| is_dt(child, "Name")) {
538 diagnostics.push(diagnostic(
539 Severity::Error,
540 DiagnosticCode::MissingName,
541 &path,
542 "ConceptType-derived element lacks a Name",
543 ));
544 }
545 match element
546 .children()
547 .filter(|child| is_dt(child, "Definition"))
548 .count()
549 {
550 0 => diagnostics.push(diagnostic(
551 Severity::Warning,
552 DiagnosticCode::MissingDefinition,
553 &path,
554 "ConceptType-derived element lacks its required Definition",
555 )),
556 1 => {}
557 _ => diagnostics.push(diagnostic(
558 Severity::Warning,
559 DiagnosticCode::DuplicateDefinition,
560 &path,
561 "ConceptType-derived element has more than one Definition",
562 )),
563 }
564 }
565 if is_reference_element(element, parent_local)
566 && element.attribute_ns(Some(NAMESPACE), "GUID").is_none()
567 && element
568 .attribute_ns(Some(NAMESPACE), "referenceURI")
569 .is_none()
570 {
571 diagnostics.push(diagnostic(
572 Severity::Warning,
573 DiagnosticCode::EmptyReference,
574 &path,
575 "reference has neither dt:GUID nor dt:referenceURI",
576 ));
577 }
578 if is_multilingual_text_element(element, parent_local) {
579 match element.attribute_ns(None, "language") {
580 None => diagnostics.push(diagnostic(
581 Severity::Error,
582 DiagnosticCode::MissingLanguage,
583 &path,
584 "multi-language text lacks its required language attribute",
585 )),
586 Some(language) if MultiLanguageText::new(language, element.direct_text()).is_err() => {
587 diagnostics.push(diagnostic(
588 Severity::Error,
589 DiagnosticCode::InvalidLanguage,
590 &path,
591 "language does not match the xs:language lexical contract",
592 ));
593 }
594 Some(_) => {}
595 }
596 }
597 if is_dt(element, "DataType") {
598 match element.attribute_ns(None, "name") {
599 None => diagnostics.push(diagnostic(
600 Severity::Warning,
601 DiagnosticCode::MissingDataTypeName,
602 &path,
603 "DataType has no name; Annex E permits it but the value is underspecified",
604 )),
605 Some(name) if matches!(DataTypeName::from(name), DataTypeName::Other(_)) => {
606 diagnostics.push(diagnostic(
607 Severity::Warning,
608 DiagnosticCode::UnknownDataType,
609 &path,
610 format!("unknown data-type name {name:?} retained"),
611 ));
612 }
613 Some(_) => {}
614 }
615 }
616
617 for child in element.children() {
618 validate_element(
619 child,
620 kind,
621 Some(element.local_name()),
622 &path,
623 guids,
624 diagnostics,
625 );
626 }
627}
628
629fn is_reference_element(element: &Element, parent_local: Option<&str>) -> bool {
630 element.namespace_uri() == Some(NAMESPACE)
631 && is_known_reference_name(element.local_name())
632 && reference_allowed(parent_local, element.local_name())
633}
634
635fn reference_allowed(parent: Option<&str>, child: &str) -> bool {
636 let concept_parent = parent.is_some_and(is_concept_parent);
637 (concept_parent
638 && matches!(
639 child,
640 "ReferenceDocumentRef" | "DictionaryRef" | "SimilarToRef" | "ReplacedObjectsRef"
641 ))
642 || (parent.is_some_and(is_subject_parent)
643 && matches!(child, "HasPartRef" | "IsSubtypeOfRef"))
644 || matches!(
645 (parent, child),
646 (
647 Some("DataTemplate"),
648 "HasObjectTypeRef" | "HasPropertyRef" | "HasGroupOfPropertiesRef"
649 ) | (Some("GroupOfProperties"), "HasPropertyRef")
650 | (
651 Some("Property"),
652 "UnitRef"
653 | "QuantityKindRef"
654 | "DimensionRef"
655 | "IsDependentOnRef"
656 | "IsSpecializationOfRef"
657 )
658 | (Some("QuantityKind"), "DimensionRef")
659 | (Some("Unit"), "DimensionRef")
660 )
661}
662
663fn is_known_reference_name(child: &str) -> bool {
664 matches!(
665 child,
666 "ReferenceDocumentRef"
667 | "DictionaryRef"
668 | "SimilarToRef"
669 | "ReplacedObjectsRef"
670 | "HasPartRef"
671 | "IsSubtypeOfRef"
672 | "HasObjectTypeRef"
673 | "HasPropertyRef"
674 | "HasGroupOfPropertiesRef"
675 | "IsSpecializationOfRef"
676 | "IsDependentOnRef"
677 | "UnitRef"
678 | "QuantityKindRef"
679 | "DimensionRef"
680 )
681}
682
683fn is_multilingual_text_element(element: &Element, parent_local: Option<&str>) -> bool {
684 element.namespace_uri() == Some(NAMESPACE)
685 && is_known_multilingual_name(element.local_name())
686 && multilingual_allowed(parent_local, element.local_name())
687}
688
689fn multilingual_allowed(parent: Option<&str>, child: &str) -> bool {
690 (parent.is_some_and(is_concept_parent)
691 && matches!(child, "Name" | "Definition" | "Description" | "Example"))
692 || matches!(
693 (parent, child),
694 (Some("Unit"), "Symbol") | (Some("PossibleValues"), "ValueList")
695 )
696}
697
698fn is_known_multilingual_name(child: &str) -> bool {
699 matches!(
700 child,
701 "Name" | "Definition" | "Description" | "Example" | "Symbol" | "ValueList"
702 )
703}
704
705fn is_concept_parent(parent: &str) -> bool {
706 matches!(
707 parent,
708 "DataTemplate"
709 | "ObjectType"
710 | "GroupOfProperties"
711 | "Property"
712 | "ReferenceDocument"
713 | "QuantityKind"
714 | "Dimension"
715 | "Unit"
716 )
717}
718
719fn is_subject_parent(parent: &str) -> bool {
720 matches!(parent, "DataTemplate" | "ObjectType" | "GroupOfProperties")
721}
722
723fn diagnostic(
724 severity: Severity,
725 code: DiagnosticCode,
726 path: impl Into<String>,
727 message: impl Into<String>,
728) -> Diagnostic {
729 Diagnostic {
730 severity,
731 code,
732 path: path.into(),
733 message: message.into(),
734 }
735}
736
737fn required_guid(element: &Element) -> Result<Guid, ValueError> {
738 Guid::from_str(
739 element
740 .attribute_ns(Some(NAMESPACE), "GUID")
741 .unwrap_or_default(),
742 )
743}
744
745fn is_dt(element: &Element, local_name: &str) -> bool {
746 element.namespace_uri() == Some(NAMESPACE) && element.local_name() == local_name
747}