1use crate::{
2 BootError, BootResponse, BoxFuture, Module, ProviderDefinition, ProviderToken, Result,
3};
4use serde::Serialize;
5use serde_json::Value;
6use std::collections::BTreeMap;
7use std::fmt;
8use std::sync::Arc;
9
10const DEFAULT_VIEW_CONTENT_TYPE: &str = "text/html; charset=utf-8";
11
12pub trait ViewEngine: Send + Sync + 'static {
14 fn render(&self, view: String, context: Value) -> BoxFuture<'static, Result<String>>;
15}
16
17impl<F, Fut> ViewEngine for F
18where
19 F: Fn(String, Value) -> Fut + Send + Sync + 'static,
20 Fut: std::future::Future<Output = Result<String>> + Send + 'static,
21{
22 fn render(&self, view: String, context: Value) -> BoxFuture<'static, Result<String>> {
23 Box::pin(self(view, context))
24 }
25}
26
27#[derive(Clone)]
29pub struct ViewRenderer {
30 engine: Arc<dyn ViewEngine>,
31 content_type: String,
32}
33
34impl ViewRenderer {
35 pub fn new<E>(engine: E) -> Self
36 where
37 E: ViewEngine,
38 {
39 Self::from_engine_arc(Arc::new(engine))
40 }
41
42 pub fn from_engine_arc(engine: Arc<dyn ViewEngine>) -> Self {
43 Self {
44 engine,
45 content_type: DEFAULT_VIEW_CONTENT_TYPE.to_string(),
46 }
47 }
48
49 pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
50 self.content_type = content_type.into();
51 self
52 }
53
54 pub fn content_type(&self) -> &str {
55 &self.content_type
56 }
57
58 pub async fn render<T>(&self, view: impl Into<String>, context: &T) -> Result<String>
59 where
60 T: Serialize,
61 {
62 let view = view.into();
63 let context = serde_json::to_value(context).map_err(|error| {
64 BootError::Internal(format!(
65 "failed to serialize view context `{view}`: {error}"
66 ))
67 })?;
68 self.render_value(view, context).await
69 }
70
71 pub async fn render_value(&self, view: impl Into<String>, context: Value) -> Result<String> {
72 self.engine.render(view.into(), context).await
73 }
74
75 pub async fn render_response<T>(
76 &self,
77 view: impl Into<String>,
78 context: &T,
79 ) -> Result<BootResponse>
80 where
81 T: Serialize,
82 {
83 self.render_response_with_status(200, view, context).await
84 }
85
86 pub async fn render_response_with_status<T>(
87 &self,
88 status: u16,
89 view: impl Into<String>,
90 context: &T,
91 ) -> Result<BootResponse>
92 where
93 T: Serialize,
94 {
95 let view = view.into();
96 let context = serde_json::to_value(context).map_err(|error| {
97 BootError::Internal(format!(
98 "failed to serialize view context `{view}`: {error}"
99 ))
100 })?;
101 self.render_value_response_with_status(status, view, context)
102 .await
103 }
104
105 pub async fn render_value_response(
106 &self,
107 view: impl Into<String>,
108 context: Value,
109 ) -> Result<BootResponse> {
110 self.render_value_response_with_status(200, view, context)
111 .await
112 }
113
114 pub async fn render_value_response_with_status(
115 &self,
116 status: u16,
117 view: impl Into<String>,
118 context: Value,
119 ) -> Result<BootResponse> {
120 let html = self.render_value(view, context).await?;
121 Ok(BootResponse::html_with_status(status, html).with_content_type(self.content_type()))
122 }
123}
124
125impl fmt::Debug for ViewRenderer {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.debug_struct("ViewRenderer")
128 .field("content_type", &self.content_type)
129 .finish_non_exhaustive()
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct ViewModule {
136 name: &'static str,
137 renderer: ViewRenderer,
138 global: bool,
139}
140
141impl ViewModule {
142 pub fn new<E>(name: &'static str, engine: E) -> Self
143 where
144 E: ViewEngine,
145 {
146 Self::from_renderer(name, ViewRenderer::new(engine))
147 }
148
149 pub fn from_renderer(name: &'static str, renderer: ViewRenderer) -> Self {
150 Self {
151 name,
152 renderer,
153 global: false,
154 }
155 }
156
157 pub fn global(mut self) -> Self {
158 self.global = true;
159 self
160 }
161
162 pub fn renderer(&self) -> ViewRenderer {
163 self.renderer.clone()
164 }
165}
166
167impl Module for ViewModule {
168 fn name(&self) -> &'static str {
169 self.name
170 }
171
172 fn providers(&self) -> Result<Vec<ProviderDefinition>> {
173 Ok(vec![ProviderDefinition::singleton(self.renderer.clone())])
174 }
175
176 fn exports(&self) -> Result<Vec<ProviderToken>> {
177 Ok(vec![ProviderToken::of::<ViewRenderer>()])
178 }
179
180 fn is_global(&self) -> bool {
181 self.global
182 }
183}
184
185#[derive(Debug, Clone, Default)]
187pub struct StringTemplateViewEngine {
188 templates: Arc<BTreeMap<String, String>>,
189}
190
191impl StringTemplateViewEngine {
192 pub fn new() -> Self {
193 Self::default()
194 }
195
196 pub fn from_templates<I, K, V>(templates: I) -> Self
197 where
198 I: IntoIterator<Item = (K, V)>,
199 K: Into<String>,
200 V: Into<String>,
201 {
202 Self {
203 templates: Arc::new(
204 templates
205 .into_iter()
206 .map(|(name, template)| (name.into(), template.into()))
207 .collect(),
208 ),
209 }
210 }
211
212 pub fn with_template(mut self, name: impl Into<String>, template: impl Into<String>) -> Self {
213 Arc::make_mut(&mut self.templates).insert(name.into(), template.into());
214 self
215 }
216
217 pub fn template(&self, name: &str) -> Option<&str> {
218 self.templates.get(name).map(String::as_str)
219 }
220}
221
222impl ViewEngine for StringTemplateViewEngine {
223 fn render(&self, view: String, context: Value) -> BoxFuture<'static, Result<String>> {
224 let template = self.templates.get(&view).cloned();
225 Box::pin(async move {
226 let Some(template) = template else {
227 return Err(BootError::NotFound(format!("view was not found: {view}")));
228 };
229 Ok(render_string_template(&template, &context))
230 })
231 }
232}
233
234fn render_string_template(template: &str, context: &Value) -> String {
235 let mut rendered = String::new();
236 let mut rest = template;
237
238 while let Some(start) = rest.find("{{") {
239 rendered.push_str(&rest[..start]);
240 let after_start = &rest[start + 2..];
241 let Some(end) = after_start.find("}}") else {
242 rendered.push_str(&rest[start..]);
243 return rendered;
244 };
245
246 let key = after_start[..end].trim();
247 if let Some(value) = context_path(context, key) {
248 rendered.push_str(&view_value_to_string(value));
249 }
250 rest = &after_start[end + 2..];
251 }
252
253 rendered.push_str(rest);
254 rendered
255}
256
257fn context_path<'a>(context: &'a Value, path: &str) -> Option<&'a Value> {
258 if path.is_empty() {
259 return None;
260 }
261
262 let mut value = context;
263 for segment in path.split('.') {
264 value = value.get(segment)?;
265 }
266 Some(value)
267}
268
269fn view_value_to_string(value: &Value) -> String {
270 match value {
271 Value::Null => String::new(),
272 Value::Bool(value) => value.to_string(),
273 Value::Number(value) => value.to_string(),
274 Value::String(value) => value.clone(),
275 Value::Array(_) | Value::Object(_) => value.to_string(),
276 }
277}