Skip to main content

a3s_boot/
view.rs

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
12/// Rendering backend for Nest-style MVC view responses.
13pub 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/// Provider that renders view names into HTML responses.
28#[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/// Module that registers a [`ViewRenderer`] provider.
134#[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/// Small string-template engine for tests and lightweight HTML responses.
186#[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}