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