schemaorg_rs/validation/diagnostics.rs
1//! Diagnostic types for Schema.org validation.
2//!
3//! [`ValidationDiagnostic`] represents a single validation finding with a
4//! JSON-path-like location, severity level, machine-readable code, and
5//! human-readable message.
6
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::types::SourceLocation;
12
13/// A single validation diagnostic (error, warning, or informational).
14///
15/// Each diagnostic describes a specific issue found during validation,
16/// with enough context for both human readers and programmatic consumers.
17///
18/// # Examples
19///
20/// ```no_run
21/// # #[cfg(feature = "validation")]
22/// # {
23/// use schemaorg_rs::validation::{ValidationDiagnostic, Severity, DiagnosticCode};
24///
25/// let diag = ValidationDiagnostic {
26/// path: "Product.offers[0].price".into(),
27/// severity: Severity::Error,
28/// code: DiagnosticCode::InvalidValueType,
29/// message: "Property 'price' expects Number or Text, got Person".into(),
30/// source_location: None,
31/// };
32///
33/// assert!(diag.severity == Severity::Error);
34/// # }
35/// ```
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[must_use]
38pub struct ValidationDiagnostic {
39 /// JSON-path-like location in the structured data graph.
40 ///
41 /// Examples: `"Product"`, `"Product.offers[0].price"`.
42 pub path: String,
43
44 /// Severity level.
45 pub severity: Severity,
46
47 /// Machine-readable diagnostic code.
48 pub code: DiagnosticCode,
49
50 /// Human-readable message describing the issue.
51 pub message: String,
52
53 /// Location in the original HTML, if available.
54 pub source_location: Option<SourceLocation>,
55}
56
57/// Severity level for validation diagnostics.
58///
59/// Ordered from most to least severe (`Error > Warning > Info`).
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61#[must_use]
62pub enum Severity {
63 /// Invalid per the Schema.org specification.
64 Error,
65 /// Deprecated, likely unintended, or potentially incorrect.
66 Warning,
67 /// Informational (e.g. pending types).
68 Info,
69}
70
71/// Machine-readable diagnostic codes.
72///
73/// Each code corresponds to a specific class of validation issue.
74/// Use these for programmatic filtering and reporting.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
76#[non_exhaustive]
77#[must_use]
78pub enum DiagnosticCode {
79 // Type-level
80 /// The `@type` value is not a known Schema.org type.
81 UnknownType,
82 /// The type has been retired to `attic.schema.org`.
83 DeprecatedType,
84 /// The type is in `pending.schema.org` (not yet stable).
85 PendingType,
86
87 // Property-level
88 /// The property name is not a known Schema.org property.
89 UnknownProperty,
90 /// The property exists but is not valid for the given type.
91 PropertyNotForType,
92 /// The property has been superseded by another.
93 DeprecatedProperty,
94 /// The property is in `pending.schema.org`.
95 PendingProperty,
96
97 // Value-level
98 /// The value type does not match any expected `rangeIncludes` type.
99 InvalidValueType,
100 /// A URL was expected but plain text was provided.
101 ExpectedUrlGotText,
102 /// A text value was expected but a nested object was provided.
103 ExpectedTextGotNode,
104 /// The value should be an enumeration member but is not.
105 InvalidEnumValue,
106 /// A boolean value was provided as a string (`"true"` / `"false"`).
107 InvalidBoolean,
108 /// A non-numeric string was provided where a number was expected.
109 InvalidNumber,
110
111 // Profile-level
112 /// A required field is missing (profile-specific).
113 RequiredFieldMissing,
114 /// A recommended field is missing (profile-specific).
115 RecommendedFieldMissing,
116 /// A required field in a nested object is missing.
117 NestedRequiredFieldMissing,
118 /// A field value does not meet profile-specific constraints.
119 InvalidFieldValue,
120 /// Eligibility is restricted by external factors (e.g. site authority).
121 EligibilityRestricted,
122}
123
124impl fmt::Display for Severity {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 Self::Error => write!(f, "error"),
128 Self::Warning => write!(f, "warning"),
129 Self::Info => write!(f, "info"),
130 }
131 }
132}
133
134impl fmt::Display for DiagnosticCode {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 let s = match self {
137 Self::UnknownType => "unknown-type",
138 Self::DeprecatedType => "deprecated-type",
139 Self::PendingType => "pending-type",
140 Self::UnknownProperty => "unknown-property",
141 Self::PropertyNotForType => "property-not-for-type",
142 Self::DeprecatedProperty => "deprecated-property",
143 Self::PendingProperty => "pending-property",
144 Self::InvalidValueType => "invalid-value-type",
145 Self::ExpectedUrlGotText => "expected-url-got-text",
146 Self::ExpectedTextGotNode => "expected-text-got-node",
147 Self::InvalidEnumValue => "invalid-enum-value",
148 Self::InvalidBoolean => "invalid-boolean",
149 Self::InvalidNumber => "invalid-number",
150 Self::RequiredFieldMissing => "required-field-missing",
151 Self::RecommendedFieldMissing => "recommended-field-missing",
152 Self::NestedRequiredFieldMissing => "nested-required-field-missing",
153 Self::InvalidFieldValue => "invalid-field-value",
154 Self::EligibilityRestricted => "eligibility-restricted",
155 };
156 write!(f, "{s}")
157 }
158}