Skip to main content

coil_template/model/
render.rs

1use super::*;
2#[cfg(test)]
3use crate::runtime::escape_html_text;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct TrustedHtml(String);
7
8impl TrustedHtml {
9    pub fn new(value: impl Into<String>) -> Result<Self, TemplateModelError> {
10        Ok(Self(require_non_empty("trusted_html", value.into())?))
11    }
12
13    pub fn as_str(&self) -> &str {
14        &self.0
15    }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum RenderValue {
20    Text(String),
21    TrustedHtml(TrustedHtml),
22    Bool(bool),
23    List(Vec<RenderModel>),
24    Object(RenderModel),
25}
26
27impl RenderValue {
28    pub fn text(value: impl Into<String>) -> Self {
29        Self::Text(value.into())
30    }
31
32    pub fn trusted_html(value: TrustedHtml) -> Self {
33        Self::TrustedHtml(value)
34    }
35
36    pub fn bool(value: bool) -> Self {
37        Self::Bool(value)
38    }
39
40    pub fn list(value: Vec<RenderModel>) -> Self {
41        Self::List(value)
42    }
43
44    pub fn object(value: RenderModel) -> Self {
45        Self::Object(value)
46    }
47
48    pub(crate) fn as_text(&self, key: &str) -> Result<&str, TemplateModelError> {
49        match self {
50            Self::Text(value) => Ok(value),
51            Self::TrustedHtml(_) => Err(TemplateModelError::ValueTypeMismatch {
52                key: key.to_string(),
53                expected: "text",
54            }),
55            Self::Bool(_) => Err(TemplateModelError::ValueTypeMismatch {
56                key: key.to_string(),
57                expected: "text",
58            }),
59            Self::List(_) => Err(TemplateModelError::ValueTypeMismatch {
60                key: key.to_string(),
61                expected: "text",
62            }),
63            Self::Object(_) => Err(TemplateModelError::ValueTypeMismatch {
64                key: key.to_string(),
65                expected: "text",
66            }),
67        }
68    }
69
70    pub(crate) fn as_bool(&self, key: &str) -> Result<bool, TemplateModelError> {
71        match self {
72            Self::Bool(value) => Ok(*value),
73            Self::Text(_) | Self::TrustedHtml(_) | Self::List(_) | Self::Object(_) => {
74                Err(TemplateModelError::ValueTypeMismatch {
75                    key: key.to_string(),
76                    expected: "bool",
77                })
78            }
79        }
80    }
81
82    pub(crate) fn as_list(&self, key: &str) -> Result<&[RenderModel], TemplateModelError> {
83        match self {
84            Self::List(value) => Ok(value.as_slice()),
85            Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::Object(_) => {
86                Err(TemplateModelError::ValueTypeMismatch {
87                    key: key.to_string(),
88                    expected: "list",
89                })
90            }
91        }
92    }
93
94    pub(crate) fn as_object(&self, key: &str) -> Result<&RenderModel, TemplateModelError> {
95        match self {
96            Self::Object(value) => Ok(value),
97            Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::List(_) => {
98                Err(TemplateModelError::ValueTypeMismatch {
99                    key: key.to_string(),
100                    expected: "object",
101                })
102            }
103        }
104    }
105
106    #[cfg(test)]
107    pub(crate) fn render_html(&self) -> String {
108        match self {
109            Self::Text(value) => escape_html_text(value),
110            Self::TrustedHtml(value) => value.as_str().to_string(),
111            Self::Bool(value) => value.to_string(),
112            Self::List(_) => String::new(),
113            Self::Object(_) => String::new(),
114        }
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct RenderModel {
120    values: BTreeMap<String, RenderValue>,
121    asset_paths: BTreeMap<String, String>,
122    translations: BTreeMap<String, String>,
123}
124
125impl RenderModel {
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    pub fn with_value(
131        mut self,
132        key: impl Into<String>,
133        value: RenderValue,
134    ) -> Result<Self, TemplateModelError> {
135        let key = validate_token("render_key", key.into())?;
136        self.values.insert(key, value);
137        Ok(self)
138    }
139
140    pub fn with_bool(
141        self,
142        key: impl Into<String>,
143        value: bool,
144    ) -> Result<Self, TemplateModelError> {
145        self.with_value(key, RenderValue::bool(value))
146    }
147
148    pub fn with_list(
149        self,
150        key: impl Into<String>,
151        value: Vec<RenderModel>,
152    ) -> Result<Self, TemplateModelError> {
153        self.with_value(key, RenderValue::list(value))
154    }
155
156    pub fn with_object(
157        self,
158        key: impl Into<String>,
159        value: RenderModel,
160    ) -> Result<Self, TemplateModelError> {
161        self.with_value(key, RenderValue::object(value))
162    }
163
164    pub fn with_asset_path(
165        mut self,
166        logical_path: impl Into<String>,
167        public_url: impl Into<String>,
168    ) -> Result<Self, TemplateModelError> {
169        let logical_path = validate_token("asset_logical_path", logical_path.into())?;
170        let public_url = require_non_empty("asset_public_url", public_url.into())?;
171        self.asset_paths.insert(logical_path, public_url);
172        Ok(self)
173    }
174
175    pub fn with_translation(
176        mut self,
177        key: impl Into<String>,
178        value: impl Into<String>,
179    ) -> Result<Self, TemplateModelError> {
180        let key = validate_token("translation_key", key.into())?;
181        let value = require_non_empty("translation_value", value.into())?;
182        self.translations.insert(key, value);
183        Ok(self)
184    }
185
186    pub(crate) fn get(&self, key: &str) -> Option<&RenderValue> {
187        if let Some(value) = self.values.get(key) {
188            return Some(value);
189        }
190
191        let (head, tail) = key.split_once('.')?;
192        let value = self.values.get(head)?;
193        value.as_object(head).ok()?.get(tail)
194    }
195
196    pub(crate) fn get_path(&self, path: &str) -> Option<&RenderValue> {
197        self.get(path)
198    }
199
200    pub(crate) fn get_asset_path(&self, logical_path: &str) -> Option<&str> {
201        self.asset_paths.get(logical_path).map(String::as_str)
202    }
203
204    pub(crate) fn get_translation(&self, key: &str) -> Option<&str> {
205        self.translations.get(key).map(String::as_str)
206    }
207
208    pub(crate) fn merged_with(&self, overlay: &RenderModel) -> RenderModel {
209        let mut values = self.values.clone();
210        values.extend(overlay.values.clone());
211        let mut asset_paths = self.asset_paths.clone();
212        asset_paths.extend(overlay.asset_paths.clone());
213        let mut translations = self.translations.clone();
214        translations.extend(overlay.translations.clone());
215        RenderModel {
216            values,
217            asset_paths,
218            translations,
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct RenderOutput {
225    pub html: String,
226}