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 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)) => 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
213fn 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}