Skip to main content

schemaorg_rs/profiles/google/
local_business.rs

1//! Google Rich Results profile for `LocalBusiness` (and subtypes).
2//!
3//! Source: <https://developers.google.com/search/docs/appearance/structured-data/local-business>
4//! Verified: 2026-04-01
5
6use crate::types::SchemaNode;
7use crate::validation::ValidationDiagnostic as VD;
8
9use super::common::{recommend_property, require_property, validate_nested};
10use crate::profiles::{NodeProfileResult, Profile, TypeEligibility};
11
12/// Google Rich Results profile for `LocalBusiness` structured data.
13///
14/// Applies to: `LocalBusiness` and all subtypes (`Restaurant`, `Store`,
15/// `MedicalClinic`, etc.) via inheritance.
16pub struct GoogleLocalBusinessProfile;
17
18impl Profile for GoogleLocalBusinessProfile {
19    fn name(&self) -> &'static str {
20        "google"
21    }
22
23    fn version(&self) -> &'static str {
24        "2026-04-01"
25    }
26
27    fn source_url(&self) -> &'static str {
28        "https://developers.google.com/search/docs/appearance/structured-data/local-business"
29    }
30
31    fn supported_types(&self) -> &[&str] {
32        &["LocalBusiness"]
33    }
34
35    fn evaluate_node(&self, node: &SchemaNode, _vocab_diagnostics: &[VD]) -> NodeProfileResult {
36        let type_name = node.types.first().map_or("LocalBusiness", |t| t.as_str());
37        let path = type_name;
38        let mut diagnostics = Vec::new();
39        let mut required_missing = Vec::new();
40        let mut recommended_missing = Vec::new();
41
42        // Required fields
43        if let Some(d) = require_property(node, "name", path) {
44            required_missing.push("name".to_string());
45            diagnostics.push(d);
46        }
47        if let Some(d) = require_property(node, "address", path) {
48            required_missing.push("address".to_string());
49            diagnostics.push(d);
50        }
51
52        // Recommended fields
53        for prop in &[
54            "image",
55            "telephone",
56            "url",
57            "openingHoursSpecification",
58            "geo",
59            "priceRange",
60        ] {
61            if let Some(d) = recommend_property(node, prop, path) {
62                recommended_missing.push((*prop).to_string());
63                diagnostics.push(d);
64            }
65        }
66
67        // Nested PostalAddress validation
68        let address_diags = validate_nested(
69            node,
70            "address",
71            "PostalAddress",
72            &[
73                "streetAddress",
74                "addressLocality",
75                "postalCode",
76                "addressCountry",
77            ],
78            &["addressRegion"],
79            path,
80        );
81        diagnostics.extend(address_diags);
82
83        let eligible = required_missing.is_empty();
84
85        NodeProfileResult {
86            type_eligibility: TypeEligibility {
87                schema_type: type_name.to_string(),
88                eligible,
89                required_missing,
90                recommended_missing,
91                field_diagnostics: diagnostics,
92            },
93        }
94    }
95}