schemaorg_rs/profiles/google/
product.rs1use crate::types::SchemaNode;
7use crate::validation::diagnostics::{DiagnosticCode, Severity, ValidationDiagnostic};
8use crate::validation::ValidationDiagnostic as VD;
9
10use super::common::{
11 get_nested_nodes, recommend_property, require_one_of, require_property, validate_nested,
12};
13use crate::profiles::{NodeProfileResult, Profile, TypeEligibility};
14
15pub struct GoogleProductProfile;
17
18impl Profile for GoogleProductProfile {
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/product"
29 }
30
31 fn supported_types(&self) -> &[&str] {
32 &["Product"]
33 }
34
35 fn evaluate_node(&self, node: &SchemaNode, _vocab_diagnostics: &[VD]) -> NodeProfileResult {
36 let path = "Product";
37 let mut diagnostics: Vec<ValidationDiagnostic> = Vec::new();
38 let mut required_missing = Vec::new();
39 let mut recommended_missing = Vec::new();
40
41 if let Some(d) = require_property(node, "name", path) {
43 required_missing.push("name".to_string());
44 diagnostics.push(d);
45 }
46
47 for prop in &["image", "description", "brand", "sku"] {
49 if let Some(d) = recommend_property(node, prop, path) {
50 recommended_missing.push((*prop).to_string());
51 diagnostics.push(d);
52 }
53 }
54
55 if let Some(d) = require_one_of(
57 node,
58 &["gtin", "gtin8", "gtin13", "gtin14", "isbn", "mpn"],
59 path,
60 Severity::Warning,
61 ) {
62 recommended_missing.push("gtin/isbn/mpn".to_string());
63 diagnostics.push(d);
64 }
65
66 if let Some(d) =
68 require_one_of(node, &["offers", "aggregateOffer"], path, Severity::Warning)
69 {
70 recommended_missing.push("offers".to_string());
71 diagnostics.push(d);
72 }
73
74 if let Some(d) = require_one_of(
76 node,
77 &["aggregateRating", "review"],
78 path,
79 Severity::Warning,
80 ) {
81 recommended_missing.push("aggregateRating/review".to_string());
82 diagnostics.push(d);
83 }
84
85 let offer_diags = validate_nested(
87 node,
88 "offers",
89 "Offer",
90 &["price", "priceCurrency", "availability"],
91 &["url", "priceValidUntil", "itemCondition"],
92 path,
93 );
94 diagnostics.extend(offer_diags);
95
96 let agg_offer_diags = validate_nested(
98 node,
99 "aggregateOffer",
100 "AggregateOffer",
101 &["lowPrice", "priceCurrency"],
102 &["highPrice"],
103 path,
104 );
105 diagnostics.extend(agg_offer_diags);
106
107 for rating_node in get_nested_nodes(node, "aggregateRating") {
109 let rating_path = format!("{path}.aggregateRating");
110 if let Some(d) = require_property(rating_node, "ratingValue", &rating_path) {
111 diagnostics.push(d);
112 }
113 if let Some(d) = require_one_of(
114 rating_node,
115 &["ratingCount", "reviewCount"],
116 &rating_path,
117 Severity::Error,
118 ) {
119 diagnostics.push(d);
120 }
121 }
122
123 let review_diags = validate_nested(
125 node,
126 "review",
127 "Review",
128 &["author"],
129 &["reviewRating"],
130 path,
131 );
132 diagnostics.extend(review_diags);
133
134 let eligible = required_missing.is_empty()
135 && !diagnostics
136 .iter()
137 .any(|d| d.code == DiagnosticCode::NestedRequiredFieldMissing);
138
139 NodeProfileResult {
140 type_eligibility: TypeEligibility {
141 schema_type: "Product".to_string(),
142 eligible,
143 required_missing,
144 recommended_missing,
145 field_diagnostics: diagnostics,
146 },
147 }
148 }
149}