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