soph_view/support/
view.rs1use 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 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 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}