Skip to main content

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}