Skip to main content

sofie_http/
screens.rs

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/// Keyed `<module>/<area>/<name>`.
18#[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
45/// With `development`, each template is read and parsed again whenever it or one of the module
46/// `components` (`(name, path)`) is edited; otherwise its compiled program is loaded. A screen
47/// keyed under another module than the one listing it replaces that module's own.
48pub 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    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    /// Skips keys the app already gave a template: that is how an app overrides a built-in screen.
124    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    /// A development build gives them again whenever it rereads the file.
161    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)) => file.current(),
204            None => Err(not_loaded(key)),
205        }
206    }
207}
208
209fn not_loaded(key: &str) -> Error {
210    Error::internal_message(format!("screen {key} is not loaded"))
211}
212
213/// Screens are described while the app is built, before any request holds one.
214fn unshared<'t>(key: &str, template: &'t mut Arc<Template>) -> Result<&'t mut Template> {
215    Arc::get_mut(template).ok_or_else(|| {
216        Error::internal_message(format!(
217            "screen {key} is described while a request renders it"
218        ))
219    })
220}
221
222#[cfg(feature = "parse")]
223fn parse_with(key: &str, source: &str, components: &sofie_engine::Components) -> Result<Template> {
224    Template::parse_with(key, source, components)
225        .map_err(|error| Error::internal(format!("parsing screen {key}"), error))
226}
227
228#[cfg(test)]
229mod tests {
230    use serde_json::json;
231
232    use super::*;
233
234    fn rendered(screens: &Screens, key: &str) -> String {
235        screens
236            .template(key)
237            .unwrap()
238            .render(&json!({"a": 1}))
239            .unwrap()
240    }
241
242    #[test]
243    fn a_key_names_one_template() {
244        let mut screens = screens_from_sources(&[("order/web/orders", "<p>{data.a}</p>")]).unwrap();
245
246        let error = screens.add_source("order/web/orders", "<p>").unwrap_err();
247
248        assert!(
249            error
250                .to_string()
251                .contains("screen order/web/orders is added twice"),
252            "{error}"
253        );
254        assert_eq!(rendered(&screens, "order/web/orders"), "<p>1</p>");
255        assert_eq!(
256            screens.template("order/web/orders").unwrap().name(),
257            "order/web/orders"
258        );
259    }
260
261    #[test]
262    fn an_app_template_overrides_a_built_in_one_under_its_key() {
263        let mut screens =
264            screens_from_sources(&[("sofie_identity/web/sign-in", "<p>app</p>")]).unwrap();
265
266        let compiled = |key, source| {
267            let program = sofie_engine::compile_template(key, source).unwrap();
268            File::new("sign.html", Vec::leak(program))
269        };
270        let sign_in = compiled("sofie_identity/web/sign-in", "<p>built in</p>");
271        let sign_up = compiled("sofie_identity/web/sign-up", "<p>{data.a}</p>");
272
273        screens
274            .add_builtin(&[
275                ("sofie_identity/web/sign-in", sign_in),
276                ("sofie_identity/web/sign-up", sign_up),
277            ])
278            .unwrap();
279
280        assert_eq!(
281            rendered(&screens, "sofie_identity/web/sign-in"),
282            "<p>app</p>"
283        );
284        assert_eq!(rendered(&screens, "sofie_identity/web/sign-up"), "<p>1</p>");
285    }
286
287    #[cfg(not(feature = "development"))]
288    #[test]
289    fn a_module_s_screen_keyed_under_another_module_replaces_that_one() {
290        let compiled = |key: &'static str, source: &str| {
291            let program = sofie_engine::compile_template(key, source).unwrap();
292            (key, File::new("screen.html", Vec::leak(program)))
293        };
294        let identity = Module {
295            screens: Vec::leak(vec![
296                compiled("sofie_identity/web/sign-in", "<p>built in</p>"),
297                compiled("sofie_identity/web/sign-up", "<p>{data.a}</p>"),
298            ]),
299            ..Module::new("sofie_identity")
300        };
301        let order = Module {
302            screens: Vec::leak(vec![compiled("sofie_identity/web/sign-in", "<p>app</p>")]),
303            ..Module::new("order")
304        };
305
306        let screens = wired_screens(&[identity, order], &[]).unwrap();
307
308        assert_eq!(
309            rendered(&screens, "sofie_identity/web/sign-in"),
310            "<p>app</p>"
311        );
312        assert_eq!(rendered(&screens, "sofie_identity/web/sign-up"), "<p>1</p>");
313    }
314
315    #[test]
316    fn a_broken_template_names_its_key() {
317        let error = screens_from_sources(&[("order/web/broken", "<p>")])
318            .err()
319            .unwrap();
320
321        assert!(
322            error
323                .to_string()
324                .starts_with("parsing screen order/web/broken: order/web/broken:1:1"),
325            "{error}"
326        );
327    }
328
329    #[test]
330    fn an_unreadable_program_names_its_key() {
331        let error = Screens::default()
332            .add(
333                "order/web/broken",
334                &File::new("broken.html", b"not a program"),
335            )
336            .unwrap_err();
337
338        assert!(
339            error
340                .to_string()
341                .starts_with("loading screen order/web/broken from broken.html"),
342            "{error}"
343        );
344    }
345}