1use std::collections::BTreeMap;
2use std::fmt;
3
4use crate::error::WasmModelError;
5use crate::validation::{require_non_empty, validate_token};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8pub enum RobotsDirective {
9 Index,
10 NoIndex,
11 Follow,
12 NoFollow,
13 NoArchive,
14}
15
16impl fmt::Display for RobotsDirective {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Index => f.write_str("index"),
20 Self::NoIndex => f.write_str("noindex"),
21 Self::Follow => f.write_str("follow"),
22 Self::NoFollow => f.write_str("nofollow"),
23 Self::NoArchive => f.write_str("noarchive"),
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum JsonLdValue {
30 String(String),
31 Number(String),
32 Bool(bool),
33 Node(JsonLdNode),
34 List(Vec<JsonLdValue>),
35}
36
37impl JsonLdValue {
38 fn render(&self) -> String {
39 match self {
40 Self::String(value) => format!("\"{}\"", escape_json(value)),
41 Self::Number(value) => value.clone(),
42 Self::Bool(value) => value.to_string(),
43 Self::Node(node) => node.render(),
44 Self::List(values) => format!(
45 "[{}]",
46 values
47 .iter()
48 .map(JsonLdValue::render)
49 .collect::<Vec<_>>()
50 .join(",")
51 ),
52 }
53 }
54
55 pub(crate) fn validate(&self) -> Result<(), WasmModelError> {
56 match self {
57 Self::String(value) => {
58 let _ = require_non_empty("json_ld_string", value.clone())?;
59 }
60 Self::Number(value) => {
61 if value
62 .parse::<f64>()
63 .ok()
64 .filter(|value| value.is_finite())
65 .is_none()
66 {
67 return Err(WasmModelError::InvalidJsonLdNumber {
68 property: "json_ld_number".to_string(),
69 value: value.clone(),
70 });
71 }
72 }
73 Self::Bool(_) => {}
74 Self::Node(node) => node.validate()?,
75 Self::List(values) => {
76 for value in values {
77 value.validate()?;
78 }
79 }
80 }
81 Ok(())
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct JsonLdNode {
87 schema_type: String,
88 properties: BTreeMap<String, JsonLdValue>,
89}
90
91impl JsonLdNode {
92 pub fn new(schema_type: impl Into<String>) -> Result<Self, WasmModelError> {
93 Ok(Self {
94 schema_type: validate_token("schema_type", schema_type.into())?,
95 properties: BTreeMap::new(),
96 })
97 }
98
99 pub fn set_string(
100 mut self,
101 property: impl Into<String>,
102 value: impl Into<String>,
103 ) -> Result<Self, WasmModelError> {
104 let property = validate_property_name(property.into())?;
105 if self.properties.contains_key(&property) {
106 return Err(WasmModelError::DuplicateJsonLdProperty { property });
107 }
108 self.properties.insert(
109 property,
110 JsonLdValue::String(require_non_empty("json_ld_string", value.into())?),
111 );
112 Ok(self)
113 }
114
115 pub fn set_number(
116 mut self,
117 property: impl Into<String>,
118 value: f64,
119 ) -> Result<Self, WasmModelError> {
120 let property = validate_property_name(property.into())?;
121 if self.properties.contains_key(&property) {
122 return Err(WasmModelError::DuplicateJsonLdProperty { property });
123 }
124 if !value.is_finite() {
125 return Err(WasmModelError::InvalidJsonLdNumber {
126 property,
127 value: value.to_string(),
128 });
129 }
130 self.properties
131 .insert(property, JsonLdValue::Number(value.to_string()));
132 Ok(self)
133 }
134
135 pub fn set_bool(
136 mut self,
137 property: impl Into<String>,
138 value: bool,
139 ) -> Result<Self, WasmModelError> {
140 let property = validate_property_name(property.into())?;
141 if self.properties.contains_key(&property) {
142 return Err(WasmModelError::DuplicateJsonLdProperty { property });
143 }
144 self.properties.insert(property, JsonLdValue::Bool(value));
145 Ok(self)
146 }
147
148 pub fn set_node(
149 mut self,
150 property: impl Into<String>,
151 node: JsonLdNode,
152 ) -> Result<Self, WasmModelError> {
153 let property = validate_property_name(property.into())?;
154 if self.properties.contains_key(&property) {
155 return Err(WasmModelError::DuplicateJsonLdProperty { property });
156 }
157 self.properties.insert(property, JsonLdValue::Node(node));
158 Ok(self)
159 }
160
161 pub fn set_list(
162 mut self,
163 property: impl Into<String>,
164 values: Vec<JsonLdValue>,
165 ) -> Result<Self, WasmModelError> {
166 let property = validate_property_name(property.into())?;
167 if self.properties.contains_key(&property) {
168 return Err(WasmModelError::DuplicateJsonLdProperty { property });
169 }
170 self.properties.insert(property, JsonLdValue::List(values));
171 Ok(self)
172 }
173
174 pub fn render(&self) -> String {
175 let mut segments = vec![format!("\"@type\":\"{}\"", escape_json(&self.schema_type))];
176 for (property, value) in &self.properties {
177 segments.push(format!("\"{}\":{}", escape_json(property), value.render()));
178 }
179 format!("{{{}}}", segments.join(","))
180 }
181
182 pub(crate) fn schema_type(&self) -> &str {
183 &self.schema_type
184 }
185
186 pub(crate) fn properties(&self) -> &BTreeMap<String, JsonLdValue> {
187 &self.properties
188 }
189
190 pub(crate) fn insert_value(
191 mut self,
192 property: impl Into<String>,
193 value: JsonLdValue,
194 ) -> Result<Self, WasmModelError> {
195 let property = validate_property_name(property.into())?;
196 if self.properties.contains_key(&property) {
197 return Err(WasmModelError::DuplicateJsonLdProperty { property });
198 }
199 self.properties.insert(property, value);
200 Ok(self)
201 }
202
203 pub(crate) fn validate(&self) -> Result<(), WasmModelError> {
204 let _ = validate_token("schema_type", self.schema_type.clone())?;
205 for (property, value) in &self.properties {
206 let _ = validate_property_name(property.clone())?;
207 value.validate()?;
208 }
209 Ok(())
210 }
211}
212
213fn validate_property_name(value: String) -> Result<String, WasmModelError> {
214 let trimmed = value.trim();
215 if trimmed.is_empty()
216 || !trimmed
217 .chars()
218 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '@' | '_' | '-'))
219 {
220 Err(WasmModelError::InvalidJsonLdProperty {
221 property: trimmed.to_string(),
222 })
223 } else {
224 Ok(trimmed.to_string())
225 }
226}
227
228fn escape_json(value: &str) -> String {
229 value.replace('\\', "\\\\").replace('"', "\\\"")
230}