Skip to main content

ravenrs_app/model/
generate.rs

1//! The lowest layer of the model, from the entities.
2
3use std::collections::BTreeSet;
4
5use super::node::ModelNode;
6use crate::{DashboardContent, DashboardInfo, EntityInfo, FieldInfo, ModelError};
7
8/// More simple fields than this and a form's group uses two columns.
9const FIELDS_IN_ONE_COLUMN: usize = 4;
10
11/// Every field gets a column, out of sight unless it belongs in this list, so showing
12/// one is a matter of its index.
13fn 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
98/// What says what a field or an entity is called in the views and the navigation came from it. A
99/// caption changed under `Entities`, as a translation does, follows there unless a view has its own.
100pub(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                // A list on its own tab is called what the field is.
123                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
142/// Gives every view of an entity the fields the layers added to it, where it has not said
143/// otherwise: a column at the end of its list, and a place on its form with the other simple fields.
144pub(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
201/// Adds a dashboard to a model: its view, and its place in the navigation if it has one.
202pub 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
255/// The form's layout: simple fields in a group, in two columns when there are many; fields
256/// that take the height they need in a group of their own; one list in a group, several in tabs.
257/// The fields of its form, those of its focus order first and in that order.
258fn 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    // Stable, so the fields it does not name keep the order they were declared in.
264    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        // In the model, a translation or an administrator can change them like the caption.
345        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
390/// The model the entities give: their members, a list, a list to pick from and a
391/// form for each, and the navigation.
392pub 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
430/// What in a model points to nothing, in words for whoever changed it: a view of an
431/// entity that is gone, a column of a field that is not there, a menu item with no view.
432pub 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}