1use std::collections::{BTreeMap, BTreeSet};
6use std::sync::{Arc, Mutex};
7
8use serde_json::{Map, Value, json};
9use sofie_core::unpoisoned;
10use sofie_core::value::serde::ser::{Serialize, SerializeMap, Serializer};
11
12use crate::catalog::{FormPart, constant_label};
13use crate::language::{Language, Texts};
14use crate::message::Recorded;
15use crate::syntax::{Expression, Op};
16use crate::walk::{Visit, walk};
17
18const ENGINE_KEYS: [&str; 4] = [
19 "table.empty",
20 "table.filter",
21 "table.previous",
22 "table.next",
23];
24
25#[must_use]
27pub fn plain_client_view(template: &crate::Template) -> Value {
28 client_parts(template).view(Language::plain()).to_value()
29}
30
31pub(crate) fn client_parts(template: &crate::Template) -> &ClientParts {
32 template
33 .precomputed
34 .client_parts
35 .get_or_init(|| ClientParts::collect(&template.ops))
36}
37
38#[derive(Debug)]
41pub(crate) struct ClientParts {
42 #[cfg(feature = "gates")]
43 gates: Vec<crate::rendering::Gate>,
44 keys: BTreeSet<String>,
45 counted: BTreeSet<String>,
46 recorded: BTreeMap<String, Recorded>,
48 forms: Vec<ClientForm>,
49 enums: BTreeSet<String>,
50 #[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
52 tables: Vec<Vec<String>>,
53 in_languages: Mutex<Vec<(Texts, LanguageView)>>,
56}
57
58type LanguageView = Arc<Map<String, Value>>;
59
60#[derive(Debug)]
61struct ClientForm {
62 id: String,
63 multipart: bool,
64 parts: Vec<ClientFormPart>,
65}
66
67#[derive(Debug)]
68enum ClientFormPart {
69 Field {
70 description: Value,
71 label: Expression,
72 },
73 Submit,
74}
75
76impl ClientParts {
77 pub(crate) fn collect(ops: &[Op]) -> Self {
78 let mut found = Found::default();
79 walk(ops, &mut found);
80 Self {
81 #[cfg(feature = "gates")]
82 gates: found.gates.into_iter().cloned().collect(),
83 keys: owned(found.keys),
84 counted: owned(found.counted),
85 recorded: found.recorded,
86 forms: merged_forms(found.forms),
87 enums: owned(found.enums),
88 #[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
89 tables: found.tables,
90 in_languages: Mutex::default(),
91 }
92 }
93
94 pub(crate) fn view(&self, language: Language<'_>) -> ClientView {
95 ClientView {
96 in_language: self.in_language(language),
97 #[cfg(feature = "gates")]
98 gates: self.gates(language),
99 }
100 }
101
102 fn in_language(&self, language: Language<'_>) -> LanguageView {
103 let texts = language.texts();
104 let mut in_languages = unpoisoned::lock(&self.in_languages);
105 if let Some((_, view)) = in_languages.iter().find(|(cached, _)| *cached == texts) {
106 return Arc::clone(view);
107 }
108 let mut view = Map::new();
109 view.insert("messages".to_owned(), self.messages(language));
110 view.insert("conventions".to_owned(), conventions(language));
111 view.insert("forms".to_owned(), self.forms(language));
112 view.insert("enums".to_owned(), self.enums(language));
113 let view = Arc::new(view);
114 in_languages.retain(|(cached, _)| cached.is_of_same_translations(texts));
115 in_languages.push((texts, Arc::clone(&view)));
116 view
117 }
118}
119
120#[derive(Debug)]
122pub struct ClientView {
123 in_language: LanguageView,
124 #[cfg(feature = "gates")]
125 gates: Value,
126}
127
128impl ClientView {
129 #[must_use]
130 pub fn to_value(&self) -> Value {
131 let view = Map::clone(&self.in_language);
132 #[cfg(feature = "gates")]
133 let view = {
134 let mut view = view;
135 view.insert("gates".to_owned(), self.gates.clone());
136 view
137 };
138 Value::Object(view)
139 }
140}
141
142impl Serialize for ClientView {
143 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
144 let mut map = serializer.serialize_map(None)?;
145 for (name, value) in self.in_language.iter() {
146 map.serialize_entry(name, value)?;
147 }
148 #[cfg(feature = "gates")]
149 map.serialize_entry("gates", &self.gates)?;
150 map.end()
151 }
152}
153
154fn owned(names: BTreeSet<&str>) -> BTreeSet<String> {
155 names.into_iter().map(str::to_owned).collect()
156}
157
158fn merged_forms(forms: Vec<&crate::catalog::Form>) -> Vec<ClientForm> {
161 let mut merged: Vec<ClientForm> = Vec::new();
162 for form in forms.into_iter().map(ClientForm::of) {
163 match merged.iter_mut().find(|known| known.id == form.id) {
164 Some(known) => known.merge(form),
165 None => merged.push(form),
166 }
167 }
168 merged
169}
170
171impl ClientForm {
172 fn merge(&mut self, other: Self) {
173 self.multipart |= other.multipart;
174 let submits = self.submit_count();
175 let mut after_fields = self
176 .parts
177 .iter()
178 .rposition(|part| matches!(part, ClientFormPart::Field { .. }))
179 .map_or(0, |index| index + 1);
180 let mut other_submits = 0;
181 for part in other.parts {
182 match part {
183 ClientFormPart::Submit => other_submits += 1,
184 ClientFormPart::Field {
185 ref description, ..
186 } if self.has_field(description) => {}
187 field @ ClientFormPart::Field { .. } => {
188 self.parts.insert(after_fields, field);
189 after_fields += 1;
190 }
191 }
192 }
193 self.parts
194 .extend((submits..other_submits).map(|_| ClientFormPart::Submit));
195 }
196
197 fn submit_count(&self) -> usize {
198 self.parts
199 .iter()
200 .filter(|part| matches!(part, ClientFormPart::Submit))
201 .count()
202 }
203
204 fn has_field(&self, description: &Value) -> bool {
205 self.parts.iter().any(|part| {
206 matches!(part, ClientFormPart::Field { description: known, .. } if known["name"] == description["name"])
207 })
208 }
209
210 fn of(form: &crate::catalog::Form) -> Self {
211 let parts = form
212 .parts
213 .iter()
214 .map(|part| match part {
215 FormPart::Field(field) => ClientFormPart::Field {
216 description: field.client_description(),
217 label: field.label.clone(),
218 },
219 FormPart::Submit(_) => ClientFormPart::Submit,
220 })
221 .collect();
222 let multipart = form
223 .parts
224 .iter()
225 .any(|part| matches!(part, FormPart::Field(field) if field.is_file()));
226 Self {
227 id: form.id.clone(),
228 multipart,
229 parts,
230 }
231 }
232
233 fn view(&self, language: Language<'_>) -> Value {
234 let mut submits = 0;
235 let parts: Vec<Value> = self
236 .parts
237 .iter()
238 .map(|part| match part {
239 ClientFormPart::Field { description, label } => {
240 let mut field = description.clone();
241 field["label"] = json!(constant_label(label, language));
242 json!({ "field": field })
243 }
244 ClientFormPart::Submit => {
245 submits += 1;
246 json!({ "submit": submits - 1 })
247 }
248 })
249 .collect();
250 json!({ "multipart": self.multipart, "parts": parts })
251 }
252}
253
254#[derive(Default)]
255struct Found<'t> {
256 #[cfg(feature = "gates")]
257 gates: Vec<&'t crate::rendering::Gate>,
258 keys: BTreeSet<&'t str>,
259 counted: BTreeSet<&'t str>,
260 recorded: BTreeMap<String, Recorded>,
261 forms: Vec<&'t crate::catalog::Form>,
262 enums: BTreeSet<&'t str>,
263 #[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
264 tables: Vec<Vec<String>>,
265}
266
267impl<'t> Visit<'t> for Found<'t> {
268 #[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
270 fn columns(&mut self, rows: &'t Expression, _columns: &'t [String]) {
271 if let Expression::Path(path) = rows
272 && path.root == crate::syntax::Root::Data
273 {
274 self.tables.push(path.fields.clone());
275 }
276 }
277
278 fn form(&mut self, form: &'t crate::catalog::Form) {
279 self.forms.push(form);
280 }
281
282 fn enumeration(&mut self, name: &'t str) {
283 self.enums.insert(name);
284 }
285
286 fn expression(&mut self, expression: &'t Expression) {
287 match expression {
288 Expression::Interpolated(parts) => {
289 for part in parts {
290 self.expression(part);
291 }
292 }
293 Expression::Call(call) => self.call(call),
294 #[cfg(feature = "gates")]
295 Expression::Gate(gate) => self.gates.push(gate),
296 Expression::Path(_)
297 | Expression::Text(_)
298 | Expression::Integer(_)
299 | Expression::Authored(_) => {}
300 }
301 }
302}
303
304#[cfg(feature = "i18n")]
305impl<'t> Found<'t> {
306 fn call(&mut self, call: &'t crate::translations::Call) {
307 use crate::translations::Call;
308 match call {
309 Call::Translate {
310 key,
311 arguments,
312 recorded,
313 ..
314 } => self.translation(key, arguments, recorded.as_ref()),
315 Call::Format { value, .. } => self.expression(value),
316 Call::Label { key, .. } => {
317 self.keys.insert(key);
318 }
319 }
320 }
321}
322
323#[cfg(not(feature = "i18n"))]
324impl<'t> Found<'t> {
325 fn call(&mut self, call: &'t crate::language::Call) {
326 use crate::language::Call;
327 match call {
328 Call::Translate {
329 key,
330 arguments,
331 recorded,
332 ..
333 } => self.translation(key, arguments, recorded.as_ref()),
334 Call::Date { value, .. } => self.expression(value),
335 }
336 }
337}
338
339impl<'t> Found<'t> {
340 fn translation(
341 &mut self,
342 key: &'t str,
343 arguments: &'t [crate::language::Argument],
344 recorded: Option<&Recorded>,
345 ) {
346 self.keys.insert(key);
347 if arguments.iter().any(|argument| argument.name == "count") {
348 self.counted.insert(key);
349 }
350 if let Some(recorded) = recorded {
351 self.recorded.insert(key.to_owned(), recorded.clone());
352 }
353 for argument in arguments {
354 self.expression(&argument.value);
355 }
356 }
357}
358
359impl ClientParts {
360 #[cfg(feature = "gates")]
361 fn gates(&self, language: Language<'_>) -> Value {
362 let mut gates = json!({ "permission": {}, "flag": {}, "entitlement": {} });
363 for gate in &self.gates {
364 gates[gate.asks.name()][gate.name.as_str()] = Value::Bool(gate.holds(language.gates()));
365 }
366 gates
367 }
368
369 #[cfg(feature = "i18n")]
371 fn messages(&self, language: Language<'_>) -> Value {
372 let plain = self
373 .keys
374 .iter()
375 .map(String::as_str)
376 .chain(ENGINE_KEYS)
377 .map(str::to_owned);
378 let others = self.counted.iter().map(|key| format!("{key}.other"));
379 let ones = self.counted.iter().filter_map(|key| {
380 let one = format!("{key}.one");
381 Some((language.own_message(&one)?, one))
382 });
383 let mut messages: Map<String, Value> = plain
384 .chain(others)
385 .filter_map(|key| Some((language.message(&key)?, key)))
386 .chain(ones)
387 .map(|(message, key)| (key, Value::String(message.written())))
388 .collect();
389 for (key, recorded) in &self.recorded {
390 if !messages.contains_key(key) && !messages.contains_key(&format!("{key}.other")) {
391 self.write_recorded(&mut messages, key, recorded);
392 }
393 }
394 Value::Object(messages)
395 }
396
397 #[cfg(not(feature = "i18n"))]
398 fn messages(&self, _language: Language<'_>) -> Value {
399 let _ = (&self.keys, ENGINE_KEYS);
400 let mut messages = Map::new();
401 for (key, recorded) in &self.recorded {
402 self.write_recorded(&mut messages, key, recorded);
403 }
404 Value::Object(messages)
405 }
406
407 #[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
410 pub(crate) fn translate_listing_errors(&self, data: &mut Value, language: Language<'_>) {
411 for path in &self.tables {
412 let errors = path
413 .iter()
414 .try_fold(&mut *data, |value, field| value.get_mut(field.as_str()))
415 .and_then(|listed| listed.get_mut(sofie_core::listing::ERRORS))
416 .and_then(Value::as_object_mut);
417 for refusal in errors.into_iter().flat_map(|errors| errors.values_mut()) {
418 translate_refusal(refusal, language);
419 }
420 }
421 }
422
423 fn write_recorded(&self, messages: &mut Map<String, Value>, key: &str, recorded: &Recorded) {
425 if !self.counted.contains(key) {
426 messages.insert(key.to_owned(), Value::String(recorded.other.written()));
427 return;
428 }
429 let other = Value::String(recorded.other.written());
430 messages.insert(format!("{key}.other"), other);
431 if let Some(one) = &recorded.one {
432 messages.insert(format!("{key}.one"), Value::String(one.written()));
433 }
434 }
435
436 fn forms(&self, language: Language<'_>) -> Value {
437 let forms: Map<String, Value> = self
438 .forms
439 .iter()
440 .map(|form| (form.id.clone(), form.view(language)))
441 .collect();
442 Value::Object(forms)
443 }
444
445 #[cfg(feature = "i18n")]
446 fn enums(&self, language: Language<'_>) -> Value {
447 let enums: Map<String, Value> = self
448 .enums
449 .iter()
450 .map(|name| {
451 let variants: Vec<Value> = language
452 .enumeration(name)
453 .unwrap_or_default()
454 .iter()
455 .map(|variant| {
456 json!({
457 "name": variant.name,
458 "label": language.text(&variant.key, &variant.humanized),
459 "class": variant.tone.class(),
460 })
461 })
462 .collect();
463 (name.clone(), Value::Array(variants))
464 })
465 .collect();
466 Value::Object(enums)
467 }
468
469 #[cfg(not(feature = "i18n"))]
470 fn enums(&self, _language: Language<'_>) -> Value {
471 let _ = &self.enums;
472 Value::Object(Map::new())
473 }
474}
475
476#[cfg(all(feature = "listing", any(feature = "gates", feature = "i18n")))]
477fn translate_refusal(refusal: &mut Value, language: Language<'_>) {
478 use sofie_core::listing::{REFUSAL_ARGUMENTS, REFUSAL_KEY, REFUSAL_MESSAGE};
479 let Some(record) = refusal.as_object_mut() else {
480 return;
481 };
482 let translated = {
483 let text = |name: &str| record.get(name).and_then(Value::as_str);
484 let (Some(key), Some(message)) = (text(REFUSAL_KEY), text(REFUSAL_MESSAGE)) else {
485 return;
486 };
487 let arguments: Vec<(&str, String)> = record
488 .get(REFUSAL_ARGUMENTS)
489 .and_then(Value::as_object)
490 .into_iter()
491 .flatten()
492 .filter_map(|(name, value)| Some((name.as_str(), value.as_str()?.to_owned())))
493 .collect();
494 language.field_error(key, &arguments, message)
495 };
496 record.insert(REFUSAL_MESSAGE.to_owned(), Value::String(translated));
497}
498
499#[cfg(feature = "i18n")]
500fn conventions(language: Language<'_>) -> Value {
501 let mut described = language.conventions().describe();
502 described["plural"] = Value::from(language.plural_rule().name());
503 described["zone"] = Value::from(sofie_core::DisplayZone::current().name());
504 described
505}
506
507#[cfg(not(feature = "i18n"))]
510fn conventions(_language: Language<'_>) -> Value {
511 json!({
512 "decimal": ".",
513 "group": null,
514 "money": "code-after",
515 "days": "iso",
516 "zone": sofie_core::DisplayZone::current().name(),
517 })
518}
519
520#[cfg(test)]
521mod tests {
522 use serde_json::{Value, json};
523 use sofie_values::{Control, FormField, Input};
524
525 use crate::Template;
526
527 #[test]
528 fn a_view_serializes_as_its_value_does() {
529 let template = Template::parse(
530 "screen",
531 "{t(\"cart.items\", count = data.n)}<Form id=\"f\" post=\"/f\">\
532 <Field name=\"note\" /></Form>",
533 )
534 .unwrap();
535 let view = super::client_parts(&template).view(crate::language::Language::plain());
536
537 assert_eq!(serde_json::to_value(&view).unwrap(), view.to_value());
538 }
539
540 #[test]
541 fn forms_sharing_an_id_in_two_branches_are_described_as_one() {
542 let template = Template::parse(
543 "screen",
544 "{#if data.a}<Form id=\"f\" post=\"/f\"><Field name=\"a\" /><Submit label=\"Go\" />\
545 </Form>{:else}<Form id=\"f\" post=\"/f\"><Field name=\"b\" /><Field name=\"a\" />\
546 <Submit label=\"Go\" /><Submit label=\"Later\" /></Form>{/if}",
547 )
548 .unwrap();
549
550 let view = super::plain_client_view(&template);
551 let parts = view["forms"]["f"]["parts"].as_array().unwrap();
552 let shapes: Vec<Value> = parts
553 .iter()
554 .map(|part| part["field"]["name"].clone())
555 .collect();
556
557 assert_eq!(shapes, [json!("a"), json!("b"), Value::Null, Value::Null]);
558 assert_eq!(parts[3], json!({ "submit": 1 }));
559 }
560
561 #[test]
562 fn a_view_names_the_forms_and_enums_a_screen_shows() {
563 let template = Template::parse(
564 "screen",
565 "<Form id=\"f\" post=\"/f\"><Field name=\"note\" required /><Submit label=\"Go\" />\
566 </Form>",
567 )
568 .unwrap();
569
570 let view = super::plain_client_view(&template);
571
572 assert_eq!(
573 view["forms"]["f"],
574 json!({
575 "multipart": false,
576 "parts": [
577 {
578 "field": {
579 "name": "note",
580 "label": "Note",
581 "control": "input",
582 "attributes": [["type", "text"]],
583 "options": [],
584 "parts": [],
585 "required": true,
586 "constraints": [],
587 },
588 },
589 { "submit": 0 },
590 ],
591 })
592 );
593 assert_eq!(view["conventions"]["decimal"], ".");
594 assert_eq!(view["conventions"]["zone"], "UTC");
595 }
596
597 #[test]
598 fn a_view_follows_a_form_described_after_it_was_first_asked() {
599 const PASSWORD: &[FormField] = &[FormField {
600 name: "password",
601 input: Input::new(Control::Password),
602 required: true,
603 constraints: &[],
604 }];
605 let mut template = Template::parse(
606 "screen",
607 "<Form id=\"f\" post=\"/f\"><Field name=\"password\" /></Form>",
608 )
609 .unwrap();
610 let field = |template: &Template| {
611 super::plain_client_view(template)["forms"]["f"]["parts"][0]["field"].clone()
612 };
613 assert_eq!(field(&template)["control"], "input");
614
615 template.describe_form("f", PASSWORD).unwrap();
616
617 assert_eq!(field(&template)["control"], "password");
618 assert_eq!(field(&template)["label"], "Password");
619 }
620}