1use std::collections::BTreeSet;
4
5use super::node::ModelNode;
6use crate::{DashboardContent, DashboardInfo, EntityInfo, FieldInfo, ModelError};
7
8const FIELDS_IN_ONE_COLUMN: usize = 4;
10
11fn list_view(entity: &EntityInfo, id: String, shown: impl Fn(&FieldInfo) -> bool) -> ModelNode {
14 let mut seen = 0_i64;
15 let mut columns = ModelNode::new("Columns");
16 for field in entity.fields.iter().filter(|f| !f.kind.is_collection()) {
17 let index = if shown(field) {
18 seen += 1;
19 seen - 1
20 } else {
21 -1
22 };
23 columns = columns
24 .child(ModelNode::new(&field.name).with("Caption", field.caption.as_str()).with("Index", index));
25 }
26 let node = ModelNode::new(id)
27 .with("Kind", "ListView")
28 .with("Entity", entity.name.as_str())
29 .with("Caption", entity.caption.as_str())
30 .child(columns);
31 let node = match &entity.list_criteria {
32 Some(criteria) => node.with("Criteria", criteria.as_str()),
33 None => node,
34 };
35 let node = if entity.list_editable { node.with("AllowEdit", true) } else { node };
36 let node = if entity.data_access == crate::DataAccess::Client {
37 node
38 } else {
39 node.with("DataAccess", entity.data_access.name())
40 };
41 let node = if entity.filters.is_empty() {
42 node
43 } else {
44 node.child(entity.filters.iter().enumerate().fold(ModelNode::new("Filters"), |all, (i, filter)| {
45 let item =
46 ModelNode::new(&filter.id).with("Caption", filter.caption.as_str()).with("Index", i as i64);
47 all.child(match &filter.criteria {
48 Some(criteria) => item.with("Filter", criteria.as_str()),
49 None => item,
50 })
51 }))
52 };
53 let node = match &entity.description {
54 Some(text) => node.with("Description", text.as_str()),
55 None => node,
56 };
57 let node = entity.editor_settings.iter().fold(node, |mut node, (settings, name, value)| {
58 if node.get(settings).is_none() {
59 node = node.child(ModelNode::new(settings));
60 }
61 if let Some(found) = node.get_mut(settings) {
62 found.properties.insert(name.clone(), serde_json::Value::String(value.clone()));
63 }
64 node
65 });
66 if entity.variants.is_empty() {
67 return node;
68 }
69 let variants =
70 entity.variants.iter().enumerate().fold(ModelNode::new("Variants"), |variants, (i, variant)| {
71 let item =
72 ModelNode::new(&variant.id).with("Caption", variant.caption.as_str()).with("Index", i as i64);
73 let item = match &variant.filter {
74 Some(filter) => item.with("Filter", filter.as_str()),
75 None => item,
76 };
77 let item = match &variant.editor {
78 Some(editor) => item.with("EditorType", editor.as_str()),
79 None => item,
80 };
81 variants.child(if variant.columns.is_empty() {
82 item
83 } else {
84 item.child(
85 variant
86 .columns
87 .iter()
88 .enumerate()
89 .fold(ModelNode::new("Columns"), |columns, (at, field)| {
90 columns.child(ModelNode::new(field).with("Index", at as i64))
91 }),
92 )
93 })
94 });
95 node.child(variants)
96}
97
98pub(crate) fn follow_captions(root: &mut ModelNode, declared: &[EntityInfo]) {
101 let Some(entities) = root.get("Entities").cloned() else { return };
102 let set = |node: &mut ModelNode, was: &str, now: &str| {
103 if node.text("Caption") == Some(was) && was != now {
104 node.properties.insert("Caption".to_owned(), serde_json::Value::String(now.to_owned()));
105 }
106 };
107 for entity in declared {
108 let Some(in_model) = entities.get(&entity.name) else { continue };
109 let caption = in_model.text("Caption").unwrap_or(&entity.caption);
110 let is_of = |view: &ModelNode| view.text("Entity") == Some(entity.name.as_str());
111 if let Some(views) = root.get_mut("Views") {
112 for view in views.children.iter_mut().filter(|view| is_of(view)) {
113 set(view, &entity.caption, caption);
114 for part in ["Columns", "Items"] {
115 let Some(items) = view.get_mut(part) else { continue };
116 for item in &mut items.children {
117 let Some(field) = entity.field_named(&item.id) else { continue };
118 let now = in_model.at(&["Members", &item.id]).and_then(|m| m.text("Caption"));
119 set(item, &field.caption, now.unwrap_or(&field.caption));
120 }
121 }
122 if let Some(tabs) = view.at_mut(&["Layout", "Tabs"]) {
124 for tab in &mut tabs.children {
125 let Some(field) = entity.field_named(&tab.id) else { continue };
126 let now = in_model.at(&["Members", &tab.id]).and_then(|m| m.text("Caption"));
127 set(tab, &field.caption, now.unwrap_or(&field.caption));
128 }
129 }
130 }
131 }
132 if let Some(navigation) = root.get_mut("NavigationItems") {
133 for place in &mut navigation.children {
134 for item in place.children.iter_mut().filter(|item| item.id == entity.name) {
135 set(item, &entity.caption, caption);
136 }
137 }
138 }
139 }
140}
141
142pub(crate) fn add_custom_fields(root: &mut ModelNode, declared: &[EntityInfo]) {
145 let Some(entities) = root.get("Entities").cloned() else { return };
146 for entity in declared {
147 let Some(members) = entities.at(&[&entity.name, "Members"]) else { continue };
148 let added: Vec<&ModelNode> =
149 members.children.iter().filter(|member| FieldInfo::is_custom(member, entity)).collect();
150 let Some(views) = root.get_mut("Views").filter(|_| !added.is_empty()) else { continue };
151 for member in added {
152 let caption = member.text("Caption").unwrap_or(&member.id);
153 for view in views.children.iter_mut() {
154 if view.id == format!("{}_DetailView", entity.name) {
155 add_to_form(view, member, caption);
156 } else if view.text("Entity") == Some(entity.name.as_str()) {
157 let shown = view.id == format!("{}_ListView", entity.name);
158 add_to_list(view, member, caption, shown);
159 }
160 }
161 }
162 }
163}
164
165fn add_to_list(view: &mut ModelNode, member: &ModelNode, caption: &str, shown: bool) {
166 let Some(columns) = view.get_mut("Columns") else { return };
167 if columns.get(&member.id).is_some() {
168 return;
169 }
170 let next = columns.children.iter().filter_map(|c| c.integer("Index")).max().unwrap_or(-1) + 1;
171 columns.children.push(
172 ModelNode::new(&member.id).with("Caption", caption).with("Index", if shown { next } else { -1 }),
173 );
174}
175
176fn add_to_form(view: &mut ModelNode, member: &ModelNode, caption: &str) {
177 let calculated = member.text("Expression").is_some();
178 if let Some(items) = view.get_mut("Items")
179 && items.get(&member.id).is_none()
180 {
181 items.children.push(
182 ModelNode::new(&member.id)
183 .with("Caption", caption)
184 .with("Editor", member.text("Kind").unwrap_or("Text"))
185 .with("ReadOnly", calculated),
186 );
187 }
188 let Some(layout) = view.get_mut("Layout") else { return };
189 if layout.get("Main").is_none() {
190 layout
191 .children
192 .push(ModelNode::new("Main").with("Kind", "Group").with("Index", -1).with("Columns", 1));
193 }
194 let Some(main) = layout.get_mut("Main") else { return };
195 if main.get(&member.id).is_none() {
196 let next = main.children.iter().filter_map(|c| c.integer("Index")).max().unwrap_or(-1) + 1;
197 main.children.push(ModelNode::new(&member.id).with("Field", member.id.as_str()).with("Index", next));
198 }
199}
200
201pub fn add_dashboard(root: &mut ModelNode, dashboard: &DashboardInfo) {
203 let items = dashboard.items.iter().enumerate().fold(ModelNode::new("Items"), |items, (i, item)| {
204 let node = ModelNode::new(&item.id).with("Caption", item.caption.as_str()).with("Index", i as i64);
205 items.child(match &item.content {
206 DashboardContent::View(view) => node.with("View", view.as_str()),
207 DashboardContent::Control(control) => node.with("Control", control.as_str()),
208 })
209 });
210 let view = ModelNode::new(format!("{}_DashboardView", dashboard.name))
211 .with("Kind", "DashboardView")
212 .with("Caption", dashboard.caption.as_str())
213 .child(items);
214 let view = if dashboard.variants.is_empty() {
215 view
216 } else {
217 view.child(dashboard.variants.iter().enumerate().fold(
218 ModelNode::new("Variants"),
219 |variants, (i, (id, caption, other))| {
220 variants.child(
221 ModelNode::new(id)
222 .with("Caption", caption.as_str())
223 .with("Index", i as i64)
224 .with("View", format!("{other}_DashboardView")),
225 )
226 },
227 ))
228 };
229 if let Some(views) = root.get_mut("Views") {
230 views.children.push(view);
231 }
232 let Some(place) = &dashboard.navigation else { return };
233 let mut item = ModelNode::new(&dashboard.name)
234 .with("Caption", dashboard.caption.as_str())
235 .with("View", format!("{}_DashboardView", dashboard.name));
236 if let Some(icon) = &dashboard.icon {
237 item = item.with("Icon", icon.as_str());
238 }
239 let Some(navigation) = root.get_mut("NavigationItems") else { return };
240 match navigation.get_mut(place) {
241 Some(existing) => existing.children.push(item),
242 None => navigation.children.push(ModelNode::new(place).with("Caption", place.as_str()).child(item)),
243 }
244}
245
246fn group(id: &str, index: i64, columns: i64, fields: &[&FieldInfo]) -> ModelNode {
247 fields.iter().enumerate().fold(
248 ModelNode::new(id).with("Kind", "Group").with("Index", index).with("Columns", columns),
249 |node, (i, field)| {
250 node.child(ModelNode::new(&field.name).with("Field", field.name.as_str()).with("Index", i as i64))
251 },
252 )
253}
254
255fn on_form(entity: &EntityInfo) -> Vec<&FieldInfo> {
259 let mut shown: Vec<&FieldInfo> = entity.fields.iter().filter(|f| f.in_detail).collect();
260 let place = |field: &FieldInfo| {
261 entity.focus_order.iter().position(|name| *name == field.name).unwrap_or(entity.focus_order.len())
262 };
263 shown.sort_by_key(|field| place(field));
265 shown
266}
267
268fn layout(entity: &EntityInfo) -> ModelNode {
269 let shown = on_form(entity);
270 let simple: Vec<&FieldInfo> = shown
271 .iter()
272 .copied()
273 .filter(|f| !f.kind.is_tall() && !f.kind.is_collection() && f.group.is_none())
274 .collect();
275 let tall: Vec<&FieldInfo> = shown.iter().copied().filter(|f| f.kind.is_tall()).collect();
276 let lists: Vec<&FieldInfo> = shown.iter().copied().filter(|f| f.kind.is_collection()).collect();
277 let mut node = ModelNode::new("Layout");
278 let mut index = 0;
279 if !simple.is_empty() {
280 let columns =
281 entity.form_columns.map_or(if simple.len() > FIELDS_IN_ONE_COLUMN { 2 } else { 1 }, |columns| {
282 i64::from(columns.max(1))
283 });
284 node = node.child(group("Main", index, columns, &simple));
285 index += 1;
286 }
287 let mut titled: Vec<&str> = Vec::new();
288 for field in &shown {
289 if let Some(title) = field.group.as_deref()
290 && !titled.contains(&title)
291 {
292 titled.push(title);
293 }
294 }
295 for title in titled {
296 let inside: Vec<&FieldInfo> = shown
297 .iter()
298 .copied()
299 .filter(|f| f.group.as_deref() == Some(title) && !f.kind.is_tall() && !f.kind.is_collection())
300 .collect();
301 let columns = if inside.len() > FIELDS_IN_ONE_COLUMN { 2 } else { 1 };
302 node = node.child(group(title, index, columns, &inside).with("Caption", title));
303 index += 1;
304 }
305 if !tall.is_empty() {
306 node = node.child(group("Long", index, 1, &tall));
307 index += 1;
308 }
309 for name in &entity.form_actions {
310 node = node.child(
311 ModelNode::new(format!("Actions_{name}"))
312 .with("Kind", "Group")
313 .with("Index", index)
314 .with("Columns", 1)
315 .child(ModelNode::new(name).with("Kind", "ActionContainer").with("Category", name.as_str())),
316 );
317 index += 1;
318 }
319 match lists.as_slice() {
320 [] => {}
321 [one] => node = node.child(group("List", index, 1, &[*one])),
322 many => {
323 let tabs = many.iter().enumerate().fold(
324 ModelNode::new("Tabs").with("Kind", "Tabs").with("Index", index),
325 |tabs, (i, field)| {
326 tabs.child(
327 group(&field.name, i as i64, 1, &[*field]).with("Caption", field.caption.as_str()),
328 )
329 },
330 );
331 node = node.child(tabs);
332 }
333 }
334 node
335}
336
337fn detail_view(entity: &EntityInfo) -> ModelNode {
338 let items = on_form(entity).into_iter().fold(ModelNode::new("Items"), |items, field| {
339 let mut item = ModelNode::new(&field.name)
340 .with("Caption", field.caption.as_str())
341 .with("Editor", field.editor.as_deref().unwrap_or(field.kind.name()))
342 .with("Wide", field.wide)
343 .with("ReadOnly", field.read_only);
344 if let Some(name) = &field.accessible_name {
346 item = item.with("AccessibleName", name.as_str());
347 }
348 if let Some(text) = &field.accessible_description {
349 item = item.with("AccessibleDescription", text.as_str());
350 }
351 items.child(item)
352 });
353 ModelNode::new(format!("{}_DetailView", entity.name))
354 .with("Kind", "DetailView")
355 .with("Entity", entity.name.as_str())
356 .with("Caption", entity.caption.as_str())
357 .child(items)
358 .child(layout(entity))
359}
360
361fn entity_node(entity: &EntityInfo) -> ModelNode {
362 let members = entity.fields.iter().fold(ModelNode::new("Members"), |members, field| {
363 let member = ModelNode::new(&field.name)
364 .with("Caption", field.caption.as_str())
365 .with("Kind", field.kind.name())
366 .with("Key", field.is_key)
367 .with("ReadOnly", field.read_only);
368 members.child(match &field.kind {
369 crate::FieldKind::Enum(options) => {
370 member.child(options.iter().fold(ModelNode::new("Options"), |all, option| {
371 all.child(ModelNode::new(&option.code).with("Caption", option.caption.as_str()))
372 }))
373 }
374 _ => member,
375 })
376 });
377 let mut node = ModelNode::new(&entity.name)
378 .with("Caption", entity.caption.as_str())
379 .child(members)
380 .child(ModelNode::new("Rules"));
381 if let Some(field) = entity.default_property() {
382 node = node.with("DefaultProperty", field.name.as_str());
383 }
384 if let Some(icon) = &entity.icon {
385 node = node.with("Icon", icon.as_str());
386 }
387 node
388}
389
390pub fn generate_model(entities: &[EntityInfo]) -> Result<ModelNode, ModelError> {
393 let mut names = BTreeSet::new();
394 for entity in entities {
395 entity.check()?;
396 if !names.insert(entity.name.as_str()) {
397 return Err(ModelError(format!("the entity {} is declared twice", entity.name)));
398 }
399 }
400 let mut objects = ModelNode::new("Entities");
401 let mut views = ModelNode::new("Views");
402 let mut navigation = ModelNode::new("NavigationItems");
403 for entity in entities {
404 objects = objects.child(entity_node(entity));
405 let lookup_field = entity.default_property().map(|f| f.name.clone());
406 views = views
407 .child(list_view(entity, format!("{}_ListView", entity.name), |f| f.in_list))
408 .child(list_view(entity, format!("{}_LookupListView", entity.name), |f| {
409 f.in_lookup || lookup_field.as_deref() == Some(&f.name)
410 }))
411 .child(detail_view(entity));
412 let Some(place) = &entity.navigation else { continue };
413 let mut item = ModelNode::new(&entity.name)
414 .with("Caption", entity.caption.as_str())
415 .with("View", format!("{}_ListView", entity.name));
416 if let Some(icon) = &entity.icon {
417 item = item.with("Icon", icon.as_str());
418 }
419 match navigation.get_mut(place) {
420 Some(existing) => existing.children.push(item),
421 None => {
422 navigation =
423 navigation.child(ModelNode::new(place).with("Caption", place.as_str()).child(item))
424 }
425 }
426 }
427 Ok(ModelNode::new("Application").child(objects).child(views).child(navigation))
428}
429
430pub fn check_model(root: &ModelNode) -> Vec<String> {
433 let mut problems = Vec::new();
434 let empty = ModelNode::default();
435 let entities = root.get("Entities").unwrap_or(&empty);
436 let views = root.get("Views").unwrap_or(&empty);
437 for view in &views.children {
438 for variant in view.get("Variants").map_or(&[][..], |n| n.children.as_slice()) {
439 if let Some(shown) = variant.text("View")
440 && views.get(shown).is_none()
441 {
442 problems.push(format!("the view {} can change to {shown}, which does not exist", view.id));
443 }
444 }
445 if view.text("Kind") == Some("DashboardView") {
446 for item in view.get("Items").map_or(&[][..], |n| n.children.as_slice()) {
447 if let Some(shown) = item.text("View")
448 && views.get(shown).is_none()
449 {
450 problems.push(format!("the dashboard {} shows {shown}, which does not exist", view.id));
451 }
452 }
453 continue;
454 }
455 let Some(name) = view.text("Entity") else {
456 problems.push(format!("the view {} says nothing about its entity", view.id));
457 continue;
458 };
459 let Some(entity) = entities.get(name) else {
460 problems.push(format!("the view {} is of {name}, which does not exist", view.id));
461 continue;
462 };
463 let members = entity.get("Members").unwrap_or(&empty);
464 for part in ["Columns", "Items"] {
465 for item in view.get(part).map_or(&[][..], |n| n.children.as_slice()) {
466 if members.get(&item.id).is_none() {
467 problems
468 .push(format!("the view {} shows {}, which {name} does not have", view.id, item.id));
469 }
470 }
471 }
472 }
473 fn walk(node: &ModelNode, views: &ModelNode, problems: &mut Vec<String>) {
474 for item in &node.children {
475 if let Some(view) = item.text("View")
476 && views.get(view).is_none()
477 {
478 problems.push(format!("the navigation item {} opens {view}, which does not exist", item.id));
479 }
480 walk(item, views, problems);
481 }
482 }
483 walk(root.get("NavigationItems").unwrap_or(&empty), views, &mut problems);
484 problems
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::FieldKind;
491
492 fn account() -> EntityInfo {
493 EntityInfo::new("Account")
494 .caption("Conta")
495 .navigation("Cadastros")
496 .icon("wallet")
497 .field(FieldInfo::new("id", FieldKind::Integer).key())
498 .field(FieldInfo::new("name", FieldKind::Text).caption("Nome").default_property())
499 .field(FieldInfo::new("notes", FieldKind::LongText))
500 .field(FieldInfo::new("kind", FieldKind::Text))
501 }
502
503 fn visible(view: &ModelNode) -> Vec<&str> {
504 view.get("Columns").map_or(vec![], |c| c.in_order().iter().map(|n| n.id.as_str()).collect())
505 }
506
507 #[test]
508 fn each_entity_gets_a_list_a_lookup_and_a_form() {
509 let root = generate_model(&[account()]).expect("valid");
510 let views = root.get("Views").expect("views");
511 assert_eq!(visible(views.get("Account_ListView").expect("list")), ["name", "kind"]);
512 assert_eq!(visible(views.get("Account_LookupListView").expect("lookup")), ["name"]);
513 let form = views.get("Account_DetailView").expect("form");
514 assert_eq!(form.text("Kind"), Some("DetailView"));
515 let id = root.at(&["Views", "Account_ListView", "Columns", "id"]).expect("a hidden column");
516 assert_eq!(id.integer("Index"), Some(-1), "out of sight, but there to be shown");
517 assert!(check_model(&root).is_empty(), "{:?}", check_model(&root));
518 }
519
520 #[test]
521 fn the_navigation_groups_entities() {
522 let other = EntityInfo::new("Bank")
523 .navigation("Cadastros")
524 .field(FieldInfo::new("id", FieldKind::Integer).key());
525 let hidden = EntityInfo::new("Log").field(FieldInfo::new("id", FieldKind::Integer).key());
526 let root = generate_model(&[account(), other, hidden]).expect("valid");
527 let group = root.at(&["NavigationItems", "Cadastros"]).expect("the group");
528 assert_eq!(group.children.len(), 2);
529 assert_eq!(group.get("Account").and_then(|n| n.text("View")), Some("Account_ListView"));
530 assert_eq!(group.get("Account").and_then(|n| n.text("Icon")), Some("wallet"));
531 assert!(root.at(&["NavigationItems", "Log"]).is_none());
532 }
533
534 fn form_layout(entity: EntityInfo) -> ModelNode {
535 layout(&entity)
536 }
537
538 #[test]
539 fn a_form_goes_to_two_columns_with_more_than_four_fields() {
540 let few =
541 (0..4).fold(account_key(), |e, i| e.field(FieldInfo::new(format!("f{i}"), FieldKind::Text)));
542 assert_eq!(form_layout(few).get("Main").and_then(|g| g.integer("Columns")), Some(1));
543 let many =
544 (0..5).fold(account_key(), |e, i| e.field(FieldInfo::new(format!("f{i}"), FieldKind::Text)));
545 assert_eq!(form_layout(many).get("Main").and_then(|g| g.integer("Columns")), Some(2));
546 }
547
548 #[test]
549 fn the_focus_order_leads_the_form_and_accessible_words_reach_the_model() {
550 let entity = account()
551 .field(FieldInfo::new("until", FieldKind::Date).caption("Até").accessible_name("Fim do período"))
552 .focus_order(["kind", "name"]);
553 let main = form_layout(entity.clone()).get("Main").cloned().expect("main");
554 let order: Vec<&str> = main.in_order().iter().map(|n| n.id.as_str()).collect();
555 assert_eq!(order, ["kind", "name", "until"]);
556 let root = generate_model(&[entity]).expect("valid");
557 let item = root.at(&["Views", "Account_DetailView", "Items", "until"]).expect("the item");
558 assert_eq!(item.text("AccessibleName"), Some("Fim do período"));
559 let wrong = account().focus_order(["nothing"]);
560 assert!(wrong.check().is_err(), "a focus order names fields that exist");
561 }
562
563 fn account_key() -> EntityInfo {
564 EntityInfo::new("A").field(FieldInfo::new("id", FieldKind::Integer).key())
565 }
566
567 #[test]
568 fn tall_fields_and_lists_get_their_own_place() {
569 let list = |name: &str| FieldInfo::new(name, FieldKind::Collection("B".into()));
570 let one = account_key().field(FieldInfo::new("notes", FieldKind::LongText)).field(list("items"));
571 let form = form_layout(one);
572 assert!(form.get("Main").is_none(), "no simple fields");
573 assert!(form.get("Long").and_then(|g| g.get("notes")).is_some());
574 assert!(form.get("List").and_then(|g| g.get("items")).is_some());
575 let two = account_key().field(list("items")).field(list("payments"));
576 let form = form_layout(two);
577 let tabs = form.get("Tabs").expect("several lists are tabs");
578 assert_eq!(tabs.children.len(), 2);
579 assert_eq!(tabs.get("payments").and_then(|g| g.text("Caption")), Some("Payments"));
580 }
581
582 #[test]
583 fn a_bad_declaration_or_a_repeated_entity_stops_it() {
584 assert!(generate_model(&[EntityInfo::new("A")]).is_err());
585 assert!(generate_model(&[account(), account()]).is_err_and(|e| e.0.contains("twice")));
586 }
587
588 #[test]
589 fn the_model_says_what_points_to_nothing() {
590 let root = generate_model(&[account()]).expect("valid");
591 let broken = ModelNode::new("Application")
592 .child(root.get("Entities").expect("entities").clone())
593 .child(
594 ModelNode::new("Views")
595 .child(ModelNode::new("X").with("Entity", "Gone"))
596 .child(ModelNode::new("Y"))
597 .child(
598 ModelNode::new("Z")
599 .with("Entity", "Account")
600 .child(ModelNode::new("Columns").child(ModelNode::new("nope"))),
601 ),
602 )
603 .child(
604 ModelNode::new("NavigationItems")
605 .child(ModelNode::new("G").child(ModelNode::new("I").with("View", "Missing"))),
606 );
607 let problems = check_model(&broken);
608 assert_eq!(problems.len(), 4, "{problems:?}");
609 assert!(problems.iter().any(|p| p.contains("Gone")));
610 assert!(problems.iter().any(|p| p.contains("nope")));
611 assert!(problems.iter().any(|p| p.contains("Missing")));
612 }
613}