Skip to main content

sova_meta/
schema.rs

1//! Typed JSON-LD schema.org helpers.
2
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5use serde_json::{json, Value};
6
7/// Convert a typed schema to JSON-LD [`Value`].
8pub trait ToJsonLd {
9    fn json_ld(&self) -> Value;
10}
11
12fn wrap<T: Serialize>(ty: &str, value: &T) -> Value {
13    let mut v = serde_json::to_value(value).unwrap_or(json!({}));
14    if let Value::Object(ref mut map) = v {
15        map.insert("@type".into(), Value::String(ty.into()));
16        map.insert(
17            "@context".into(),
18            Value::String("https://schema.org".into()),
19        );
20    }
21    v
22}
23
24#[derive(Debug, Clone, Serialize, Default)]
25pub struct Article {
26    pub headline: String,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub author: Option<String>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub date_published: Option<DateTime<Utc>>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub image: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub description: Option<String>,
35}
36
37impl ToJsonLd for Article {
38    fn json_ld(&self) -> Value {
39        wrap("Article", self)
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Default)]
44pub struct Product {
45    pub name: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub description: Option<String>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub image: Option<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub sku: Option<String>,
52}
53
54impl ToJsonLd for Product {
55    fn json_ld(&self) -> Value {
56        wrap("Product", self)
57    }
58}
59
60#[derive(Debug, Clone, Serialize, Default)]
61pub struct Offer {
62    pub price: String,
63    pub price_currency: String,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub availability: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub url: Option<String>,
68}
69
70impl ToJsonLd for Offer {
71    fn json_ld(&self) -> Value {
72        wrap("Offer", self)
73    }
74}
75
76#[derive(Debug, Clone, Serialize, Default)]
77pub struct ListItem {
78    pub position: u32,
79    pub name: String,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub item: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Default)]
85pub struct BreadcrumbList {
86    pub item_list_element: Vec<ListItem>,
87}
88
89impl BreadcrumbList {
90    pub fn from_pairs(crumbs: &[(&str, &str)]) -> Self {
91        Self {
92            item_list_element: crumbs
93                .iter()
94                .enumerate()
95                .map(|(i, (name, url))| ListItem {
96                    position: (i + 1) as u32,
97                    name: (*name).to_string(),
98                    item: Some((*url).to_string()),
99                })
100                .collect(),
101        }
102    }
103}
104
105impl ToJsonLd for BreadcrumbList {
106    fn json_ld(&self) -> Value {
107        wrap("BreadcrumbList", self)
108    }
109}
110
111#[derive(Debug, Clone, Serialize, Default)]
112pub struct Organization {
113    pub name: String,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub url: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub logo: Option<String>,
118}
119
120impl ToJsonLd for Organization {
121    fn json_ld(&self) -> Value {
122        wrap("Organization", self)
123    }
124}
125
126#[derive(Debug, Clone, Serialize, Default)]
127pub struct WebSite {
128    pub name: String,
129    pub url: String,
130}
131
132impl ToJsonLd for WebSite {
133    fn json_ld(&self) -> Value {
134        wrap("WebSite", self)
135    }
136}
137
138#[derive(Debug, Clone, Serialize, Default)]
139pub struct FAQPage {
140    pub main_entity: Vec<FaqEntry>,
141}
142
143#[derive(Debug, Clone, Serialize, Default)]
144pub struct FaqEntry {
145    pub name: String,
146    pub accepted_answer: FaqAnswer,
147}
148
149#[derive(Debug, Clone, Serialize, Default)]
150pub struct FaqAnswer {
151    pub text: String,
152}
153
154impl ToJsonLd for FAQPage {
155    fn json_ld(&self) -> Value {
156        let entities: Vec<Value> = self
157            .main_entity
158            .iter()
159            .map(|e| {
160                json!({
161                    "@type": "Question",
162                    "name": e.name,
163                    "acceptedAnswer": {
164                        "@type": "Answer",
165                        "text": e.accepted_answer.text,
166                    }
167                })
168            })
169            .collect();
170        json!({
171            "@context": "https://schema.org",
172            "@type": "FAQPage",
173            "mainEntity": entities,
174        })
175    }
176}
177
178#[derive(Debug, Clone, Serialize, Default)]
179pub struct LocalBusiness {
180    pub name: String,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub address: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub telephone: Option<String>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub url: Option<String>,
187}
188
189impl ToJsonLd for LocalBusiness {
190    fn json_ld(&self) -> Value {
191        wrap("LocalBusiness", self)
192    }
193}