soph_view/support/
view.rs

1use crate::{config, View, ViewResult};
2use soph_config::support::config;
3use std::{collections::HashMap, ops::Deref};
4use tera::Tera;
5
6impl View {
7    pub fn new() -> ViewResult<Self> {
8        let config = config().parse::<config::View>()?;
9
10        let mut tera = Tera::new(&config.path)?;
11        tera.register_function("asset", asset(config.asset.url.to_owned()));
12        tera.register_function("vite", vite(config.asset.url.to_owned()));
13
14        Ok(Self { engine: tera })
15    }
16}
17
18fn asset(url: String) -> impl tera::Function {
19    Box::new(
20        move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
21            let resource = match args.get("resource") {
22                Some(val) => match serde_json::from_value::<String>(val.clone()) {
23                    Ok(v) => v,
24                    Err(_) => {
25                        let msg =
26                            format!("Function `asset` received resource={val} but `resource` can only be a string",);
27
28                        tracing::error!(err.msg = msg, "error");
29                        return Err(tera::Error::msg(msg));
30                    }
31                },
32                None => {
33                    let msg = "Function `asset` didn't receive a `resource` argument";
34
35                    tracing::error!(err.msg = msg, "error");
36                    return Err(tera::Error::msg(msg));
37                }
38            };
39
40            Ok(serde_json::Value::String(format!("{url}/{resource}")))
41        },
42    )
43}
44
45fn vite(url: String) -> impl tera::Function {
46    Box::new(
47        move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
48            let entry = match args.get("entry") {
49                Some(val) => match serde_json::from_value::<String>(val.clone()) {
50                    Ok(v) => v,
51                    Err(_) => {
52                        let msg = format!("Function `vite` received entry={val} but `entry` can only be a string",);
53
54                        tracing::error!(err.msg = msg, "error");
55                        return Err(tera::Error::msg(msg));
56                    }
57                },
58                None => {
59                    let msg = "Function `vite` didn't receive a `entry` argument";
60
61                    tracing::error!(err.msg = msg, "error");
62                    return Err(tera::Error::msg(msg));
63                }
64            };
65
66            // dev
67            if std::path::Path::new("public/hot").exists() {
68                let dev = format!(
69                    r#"<script type="module" src="http://localhost:5173/@vite/client"></script>
70                   <script type="module" src="http://localhost:5173/{}"></script>"#,
71                    &entry
72                );
73
74                return Ok(serde_json::Value::String(dev));
75            }
76
77            // build
78            let manifest = match std::fs::read_to_string("public/build/manifest.json").ok() {
79                None => {
80                    let msg = format!("Vite manifest not found at `{}`", "public/build/manifest.json");
81
82                    tracing::error!(err.msg = msg, "error");
83                    return Err(tera::Error::msg(msg));
84                }
85                Some(content) => match serde_json::from_str::<serde_json::Value>(&content)?.get(&entry) {
86                    None => {
87                        let msg = format!("Vite manifest entry not found at `{}`", &entry);
88
89                        tracing::error!(err.msg = msg, "error");
90                        return Err(tera::Error::msg(msg));
91                    }
92                    Some(val) => {
93                        if let Some(is_entry) = val.get("isEntry") {
94                            if !is_entry
95                                .as_bool()
96                                .ok_or_else(|| tera::Error::msg("Failed to parse `isEntry` as bool"))?
97                            {
98                                let msg = format!("Vite manifest entry `{}` is not an entry", &entry);
99
100                                tracing::error!(err.msg = msg, "error");
101                                return Err(tera::Error::msg(msg));
102                            }
103                        }
104
105                        val.clone()
106                    }
107                },
108            };
109
110            let mut resources = String::new();
111            if let Some(css) = manifest.get("css") {
112                for css in css
113                    .as_array()
114                    .ok_or_else(|| tera::Error::msg("Failed to parse `css` as array"))?
115                {
116                    resources.push_str(&format!(
117                        r#"<link rel="stylesheet" href="{}/build/{}">"#,
118                        url,
119                        css.as_str()
120                            .ok_or_else(|| tera::Error::msg("Failed to parse `css` as string"))?
121                    ));
122                }
123            }
124
125            if let Some(js) = manifest.get("file") {
126                resources.push_str(&format!(
127                    r#"<script type="module" src="{}/build/{}"></script>"#,
128                    url,
129                    js.as_str()
130                        .ok_or_else(|| tera::Error::msg("Failed to parse `file` as string"))?
131                ));
132            }
133
134            Ok(serde_json::Value::String(resources))
135        },
136    )
137}
138
139impl Deref for View {
140    type Target = Tera;
141
142    fn deref(&self) -> &Self::Target {
143        &self.engine
144    }
145}