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    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    /// 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)) => {
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
215/// Screens are described while the app is built, before any request holds one.
216fn 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}