Skip to main content

schemaorg_rs/profiles/
baseline.rs

1//! Baseline Schema.org profile -- generic best-practices validation.
2//!
3//! Not platform-specific. Checks common quality signals that every
4//! structured data implementation should satisfy.
5
6use crate::types::{SchemaNode, SchemaValue};
7use crate::validation::diagnostics::{DiagnosticCode, Severity, ValidationDiagnostic};
8
9use super::google::common::{has_non_empty_property, recommend_property, require_one_of};
10use super::{NodeProfileResult, Profile, TypeEligibility};
11
12/// Baseline profile checking generic Schema.org quality signals.
13pub struct BaselineProfile;
14
15impl Profile for BaselineProfile {
16    fn name(&self) -> &'static str {
17        "baseline"
18    }
19
20    fn version(&self) -> &'static str {
21        "2026-04-01"
22    }
23
24    fn source_url(&self) -> &'static str {
25        "https://schema.org/docs/gs.html"
26    }
27
28    fn supported_types(&self) -> &[&str] {
29        // Matches all types -- use a wildcard-like approach
30        &["Thing"]
31    }
32
33    fn evaluate_node(
34        &self,
35        node: &SchemaNode,
36        _vocab_diagnostics: &[ValidationDiagnostic],
37    ) -> NodeProfileResult {
38        let type_name = node.types.first().map_or("Thing", |t| t.as_str());
39        let path = type_name;
40        let mut diagnostics = Vec::new();
41        let mut recommended_missing = Vec::new();
42
43        // Every node should have name or headline
44        if let Some(d) = require_one_of(node, &["name", "headline"], path, Severity::Warning) {
45            recommended_missing.push("name/headline".to_string());
46            diagnostics.push(d);
47        }
48
49        // image is recommended on all top-level types
50        if let Some(d) = recommend_property(node, "image", path) {
51            recommended_missing.push("image".to_string());
52            diagnostics.push(d);
53        }
54
55        // description is recommended on all top-level types
56        if let Some(d) = recommend_property(node, "description", path) {
57            recommended_missing.push("description".to_string());
58            diagnostics.push(d);
59        }
60
61        // Check for HTTPS in URL properties
62        check_url_https(node, "url", path, &mut diagnostics);
63        check_url_https(node, "image", path, &mut diagnostics);
64
65        let eligible = !diagnostics.iter().any(|d| d.severity == Severity::Error);
66
67        NodeProfileResult {
68            type_eligibility: TypeEligibility {
69                schema_type: type_name.to_string(),
70                eligible,
71                required_missing: Vec::new(),
72                recommended_missing,
73                field_diagnostics: diagnostics,
74            },
75        }
76    }
77}
78
79/// Checks that URL-typed properties use HTTPS when present.
80fn check_url_https(
81    node: &SchemaNode,
82    prop: &str,
83    path: &str,
84    diagnostics: &mut Vec<ValidationDiagnostic>,
85) {
86    if !has_non_empty_property(node, prop) {
87        return;
88    }
89
90    if let Some(values) = node.properties.get(prop) {
91        for value in values {
92            if let SchemaValue::Url(url) = value {
93                if url.starts_with("http://") {
94                    diagnostics.push(ValidationDiagnostic {
95                        path: format!("{path}.{prop}"),
96                        severity: Severity::Warning,
97                        code: DiagnosticCode::InvalidFieldValue,
98                        message: format!("URL should use HTTPS instead of HTTP: {url}"),
99                        source_location: None,
100                    });
101                }
102            }
103        }
104    }
105}