ayun_view/support/
view.rs

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