Skip to main content

coil_wasm/output/
json_ld.rs

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}