1#[cfg(feature = "development")]
2mod development;
3
4#[cfg(feature = "development")]
5use development::add_wired;
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use sofie_core::{Error, Result};
11use sofie_db::File;
12use sofie_engine::{Template, load_template};
13use sofie_values::{FormField, FormFields};
14
15use crate::Module;
16
17#[derive(Default)]
19pub struct Screens {
20 by_key: HashMap<String, Screen>,
21 #[cfg(feature = "i18n")]
22 pub(crate) translations: Option<sofie_engine::Translations>,
23 #[cfg(feature = "islands")]
24 pub(crate) islands: crate::islands::Islands,
25 #[cfg(feature = "clients")]
26 pub(crate) clients: Vec<sofie_engine::ClientBundle>,
27 #[cfg(feature = "development")]
28 component_files: development::ComponentFiles,
29}
30
31enum Screen {
32 Embedded(Arc<Template>),
33 #[cfg(feature = "development")]
34 File(development::TemplateFile),
35}
36
37type FormDescription = (String, &'static [FormField]);
38
39fn describe(key: &str, template: &mut Template, (form, fields): &FormDescription) -> Result<()> {
40 template
41 .describe_form(form, fields)
42 .map_err(|error| Error::internal(format!("describing screen {key}"), error))
43}
44
45pub fn wired_screens(modules: &[Module], components: &[(&str, &str)]) -> Result<Screens> {
49 let mut screens = Screens::default();
50 #[cfg(feature = "development")]
51 {
52 screens.component_files = components
53 .iter()
54 .map(|(name, file)| ((*name).to_owned(), std::path::PathBuf::from(file)))
55 .collect();
56 }
57 #[cfg(not(feature = "development"))]
58 let _ = components;
59 for (key, file) in listed(modules, false) {
60 add_wired(&mut screens, key, file)?;
61 }
62 for (key, file) in listed(modules, true) {
63 if !screens.contains(key) {
64 add_wired(&mut screens, key, file)?;
65 }
66 }
67 for module in modules {
68 screens.describe_forms(module.forms)?;
69 }
70 Ok(screens)
71}
72
73fn listed(modules: &[Module], own: bool) -> impl Iterator<Item = &(&'static str, File)> {
74 modules.iter().flat_map(move |module| {
75 module
76 .screens
77 .iter()
78 .filter(move |(key, _)| (key.split('/').next() == Some(module.name)) == own)
79 })
80}
81
82#[derive(Clone, Copy)]
83pub struct Form {
84 screen: &'static str,
85 id: &'static str,
86 fields: &'static [FormField],
87}
88
89impl Form {
90 #[must_use]
91 pub const fn of<I: FormFields>(screen: &'static str, id: &'static str) -> Self {
92 Self {
93 screen,
94 id,
95 fields: I::FIELDS,
96 }
97 }
98}
99
100#[cfg(not(feature = "development"))]
101fn add_wired(screens: &mut Screens, key: &str, file: &File) -> Result<()> {
102 screens.add(key, file)
103}
104
105#[cfg(feature = "parse")]
106pub fn screens_from_sources(sources: &[(&str, &str)]) -> Result<Screens> {
107 let mut screens = Screens::default();
108 for (key, source) in sources {
109 screens.add_source(key, source)?;
110 }
111 Ok(screens)
112}
113
114impl Screens {
115 pub(crate) fn add(&mut self, key: &str, file: &File) -> Result<()> {
116 self.add_with(key, || {
117 load_template(key, file.bytes).map_err(|error| {
118 Error::internal(format!("loading screen {key} from {}", file.path), error)
119 })
120 })
121 }
122
123 pub fn add_builtin(&mut self, files: &[(&str, File)]) -> Result<()> {
125 for (key, file) in files {
126 if !self.contains(key) {
127 self.add(key, file)?;
128 }
129 }
130 Ok(())
131 }
132
133 #[cfg(feature = "parse")]
134 pub fn add_source(&mut self, key: &str, source: &str) -> Result<()> {
135 self.add_with(key, || {
136 parse_with(key, source, &sofie_engine::Components::default())
137 })
138 }
139
140 pub(crate) fn add_with(
141 &mut self,
142 key: &str,
143 template: impl FnOnce() -> Result<Template>,
144 ) -> Result<()> {
145 self.refuse_duplicate(key)?;
146 self.by_key
147 .insert(key.to_owned(), Screen::Embedded(Arc::new(template()?)));
148 Ok(())
149 }
150
151 fn refuse_duplicate(&self, key: &str) -> Result<()> {
152 if !self.contains(key) {
153 return Ok(());
154 }
155 Err(Error::internal_message(format!(
156 "screen {key} is added twice; a key names exactly one template"
157 )))
158 }
159
160 pub fn describe_form<I: FormFields>(&mut self, key: &str, form: &str) -> Result<()> {
162 self.describe_fields(key, form, I::FIELDS)
163 }
164
165 pub fn describe_forms(&mut self, forms: &[Form]) -> Result<()> {
166 for form in forms {
167 self.describe_fields(form.screen, form.id, form.fields)?;
168 }
169 Ok(())
170 }
171
172 #[cfg_attr(
173 not(feature = "development"),
174 expect(
175 clippy::manual_let_else,
176 reason = "a development build matches files too"
177 )
178 )]
179 pub(crate) fn describe_fields(
180 &mut self,
181 key: &str,
182 form: &str,
183 fields: &'static [FormField],
184 ) -> Result<()> {
185 let description = (form.to_owned(), fields);
186 let template = match self.by_key.get_mut(key) {
187 Some(Screen::Embedded(template)) => template,
188 #[cfg(feature = "development")]
189 Some(Screen::File(file)) => file.describe(description.clone()),
190 None => return Err(not_loaded(key)),
191 };
192 describe(key, unshared(key, template)?, &description)
193 }
194
195 pub(crate) fn contains(&self, key: &str) -> bool {
196 self.by_key.contains_key(key)
197 }
198
199 pub(crate) fn template(&self, key: &str) -> Result<Arc<Template>> {
200 match self.by_key.get(key) {
201 Some(Screen::Embedded(template)) => Ok(Arc::clone(template)),
202 #[cfg(feature = "development")]
203 Some(Screen::File(file)) => {
204 file.current(|template| development::check_reread(self, template))
205 }
206 None => Err(not_loaded(key)),
207 }
208 }
209}
210
211fn not_loaded(key: &str) -> Error {
212 Error::internal_message(format!("screen {key} is not loaded"))
213}
214
215fn unshared<'t>(key: &str, template: &'t mut Arc<Template>) -> Result<&'t mut Template> {
217 Arc::get_mut(template).ok_or_else(|| {
218 Error::internal_message(format!(
219 "screen {key} is described while a request renders it"
220 ))
221 })
222}
223
224#[cfg(feature = "parse")]
225fn parse_with(key: &str, source: &str, components: &sofie_engine::Components) -> Result<Template> {
226 Template::parse_with(key, source, components)
227 .map_err(|error| Error::internal(format!("parsing screen {key}"), error))
228}
229
230#[cfg(test)]
231mod tests {
232 use serde_json::json;
233
234 use super::*;
235
236 fn rendered(screens: &Screens, key: &str) -> String {
237 screens
238 .template(key)
239 .unwrap()
240 .render(&json!({"a": 1}))
241 .unwrap()
242 }
243
244 #[test]
245 fn a_key_names_one_template() {
246 let mut screens = screens_from_sources(&[("order/web/orders", "<p>{data.a}</p>")]).unwrap();
247
248 let error = screens.add_source("order/web/orders", "<p>").unwrap_err();
249
250 assert!(
251 error
252 .to_string()
253 .contains("screen order/web/orders is added twice"),
254 "{error}"
255 );
256 assert_eq!(rendered(&screens, "order/web/orders"), "<p>1</p>");
257 assert_eq!(
258 screens.template("order/web/orders").unwrap().name(),
259 "order/web/orders"
260 );
261 }
262
263 #[test]
264 fn an_app_template_overrides_a_built_in_one_under_its_key() {
265 let mut screens =
266 screens_from_sources(&[("sofie_identity/web/sign-in", "<p>app</p>")]).unwrap();
267
268 let compiled = |key, source| {
269 let program = sofie_engine::compile_template(key, source).unwrap();
270 File::new("sign.html", Vec::leak(program))
271 };
272 let sign_in = compiled("sofie_identity/web/sign-in", "<p>built in</p>");
273 let sign_up = compiled("sofie_identity/web/sign-up", "<p>{data.a}</p>");
274
275 screens
276 .add_builtin(&[
277 ("sofie_identity/web/sign-in", sign_in),
278 ("sofie_identity/web/sign-up", sign_up),
279 ])
280 .unwrap();
281
282 assert_eq!(
283 rendered(&screens, "sofie_identity/web/sign-in"),
284 "<p>app</p>"
285 );
286 assert_eq!(rendered(&screens, "sofie_identity/web/sign-up"), "<p>1</p>");
287 }
288
289 #[cfg(not(feature = "development"))]
290 #[test]
291 fn a_module_s_screen_keyed_under_another_module_replaces_that_one() {
292 let compiled = |key: &'static str, source: &str| {
293 let program = sofie_engine::compile_template(key, source).unwrap();
294 (key, File::new("screen.html", Vec::leak(program)))
295 };
296 let identity = Module {
297 screens: Vec::leak(vec![
298 compiled("sofie_identity/web/sign-in", "<p>built in</p>"),
299 compiled("sofie_identity/web/sign-up", "<p>{data.a}</p>"),
300 ]),
301 ..Module::new("sofie_identity")
302 };
303 let order = Module {
304 screens: Vec::leak(vec![compiled("sofie_identity/web/sign-in", "<p>app</p>")]),
305 ..Module::new("order")
306 };
307
308 let screens = wired_screens(&[identity, order], &[]).unwrap();
309
310 assert_eq!(
311 rendered(&screens, "sofie_identity/web/sign-in"),
312 "<p>app</p>"
313 );
314 assert_eq!(rendered(&screens, "sofie_identity/web/sign-up"), "<p>1</p>");
315 }
316
317 #[test]
318 fn a_broken_template_names_its_key() {
319 let error = screens_from_sources(&[("order/web/broken", "<p>")])
320 .err()
321 .unwrap();
322
323 assert!(
324 error
325 .to_string()
326 .starts_with("parsing screen order/web/broken: order/web/broken:1:1"),
327 "{error}"
328 );
329 }
330
331 #[test]
332 fn an_unreadable_program_names_its_key() {
333 let error = Screens::default()
334 .add(
335 "order/web/broken",
336 &File::new("broken.html", b"not a program"),
337 )
338 .unwrap_err();
339
340 assert!(
341 error
342 .to_string()
343 .starts_with("loading screen order/web/broken from broken.html"),
344 "{error}"
345 );
346 }
347}