Skip to main content

ravenrs_app/
entity.rs

1//! What a record is: its fields, what they hold and where they show.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use ravenrs_criteria::{Criteria, Environment, FieldType, Record, Schema, Scope, Value};
7
8/// What a field holds, which decides how it is edited and stored.
9#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
12pub enum FieldKind {
13    /// A line of text.
14    Text,
15    /// Text of any length, edited in several lines.
16    LongText,
17    /// A whole number.
18    Integer,
19    /// A number, with these decimal places.
20    Decimal(u8),
21    /// Money, in whole cents, so it is always exact.
22    Money,
23    /// A date.
24    Date,
25    /// A date and a time.
26    DateTime,
27    /// Yes or no.
28    Boolean,
29    /// One of these options.
30    Enum(Vec<EnumOption>),
31    /// Another record, by the name of its entity. It holds that record's key.
32    Reference(String),
33    /// The records of another entity that point to this one.
34    Collection(String),
35    /// A color, as `#RRGGBB`.
36    Color,
37    /// A picture.
38    Image,
39    /// A file.
40    File,
41    /// An expression of the criteria language.
42    Criteria,
43    /// A key made up when a record is created, the same everywhere: no two machines repeat one.
44    Guid,
45    /// Several of these options at once, kept as their codes separated by commas.
46    Tags(Vec<EnumOption>),
47}
48
49#[cfg_attr(feature = "live", ravenrs_live::expose)]
50impl FieldKind {
51    /// How expressions see it.
52    fn field_type(&self) -> FieldType {
53        match self {
54            Self::Text
55            | Self::LongText
56            | Self::Color
57            | Self::Criteria
58            | Self::Enum(_)
59            | Self::Guid
60            | Self::Tags(_) => FieldType::Text,
61            Self::Integer | Self::Decimal(_) | Self::Money => FieldType::Number,
62            Self::Date | Self::DateTime => FieldType::Date,
63            Self::Boolean => FieldType::Boolean,
64            Self::Reference(_) | Self::Collection(_) | Self::Image | Self::File => FieldType::Any,
65        }
66    }
67
68    /// Whether its editor takes the height it needs, instead of one line.
69    pub(crate) fn is_tall(&self) -> bool {
70        matches!(self, Self::LongText | Self::Image | Self::File | Self::Criteria)
71    }
72
73    /// Whether it lists other records instead of holding a value.
74    pub(crate) fn is_collection(&self) -> bool {
75        matches!(self, Self::Collection(_))
76    }
77
78    /// Its name in the model.
79    pub(crate) fn name(&self) -> &'static str {
80        match self {
81            Self::Text => "Text",
82            Self::LongText => "LongText",
83            Self::Integer => "Integer",
84            Self::Decimal(_) => "Decimal",
85            Self::Money => "Money",
86            Self::Date => "Date",
87            Self::DateTime => "DateTime",
88            Self::Boolean => "Boolean",
89            Self::Enum(_) => "Enum",
90            Self::Reference(_) => "Reference",
91            Self::Collection(_) => "Collection",
92            Self::Color => "Color",
93            Self::Image => "Image",
94            Self::File => "File",
95            Self::Criteria => "Criteria",
96            Self::Guid => "Guid",
97            Self::Tags(_) => "Tags",
98        }
99    }
100}
101
102/// One of the values of a [`FieldKind::Enum`].
103#[derive(Clone, Debug, PartialEq, Eq)]
104#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
105pub struct EnumOption {
106    /// What is stored.
107    pub code: String,
108    /// What people see.
109    pub caption: String,
110}
111
112#[cfg_attr(feature = "live", ravenrs_live::expose)]
113impl EnumOption {
114    /// An option stored as `code` and shown as `caption`.
115    pub fn new(code: impl Into<String>, caption: impl Into<String>) -> Self {
116        Self { code: code.into(), caption: caption.into() }
117    }
118}
119
120/// A field of an [`EntityInfo`].
121#[derive(Clone, Debug, PartialEq)]
122#[non_exhaustive]
123#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
124pub struct FieldInfo {
125    /// Its name, in expressions and in the model.
126    pub name: String,
127    /// What people see. Follows from the name unless given.
128    pub caption: String,
129    /// What it holds.
130    pub kind: FieldKind,
131    /// It identifies the record.
132    pub is_key: bool,
133    /// It can be seen but not changed.
134    pub read_only: bool,
135    /// It has a column in lists.
136    pub in_list: bool,
137    /// It is on the form.
138    pub in_detail: bool,
139    /// It has a column when picking a record of this entity.
140    pub in_lookup: bool,
141    /// An expression that gives its value; nothing is stored.
142    pub calculated: Option<String>,
143    /// What a new record starts with.
144    pub default_value: Value,
145    /// The control that edits it, by name, instead of the one for its kind.
146    pub editor: Option<String>,
147    /// What an empty editor shows.
148    pub placeholder: Option<String>,
149    /// It takes the whole width of its group on a form.
150    pub wide: bool,
151    /// Takes the keyboard when its form opens.
152    pub autofocus: bool,
153    /// What the form says under it.
154    pub hint: Option<String>,
155    /// For a reference: what its records have to meet, where `?field` is the value of that field
156    /// of the record being edited.
157    pub lookup_filter: Option<String>,
158    /// For a reference that can be left empty and is picked from a list: what empty is called.
159    pub none_caption: Option<String>,
160    /// For a collection: the field of the other entity that points back here, when it has
161    /// more than one reference to this entity.
162    pub back_reference: Option<String>,
163    /// It stays behind when a record is cloned.
164    pub non_cloneable: bool,
165    /// For a collection: its records belong to this one, and are cloned with it.
166    pub aggregated: bool,
167    /// For a picture or a file: the largest taken, in bytes.
168    pub most_bytes: Option<u64>,
169    /// For a file: the extensions offered; none means any.
170    pub accepted: Vec<String>,
171    /// The titled group it goes in on a form, away from the other fields.
172    pub group: Option<String>,
173    /// How its value shows when it is not being edited: `{}` is the value, `{:.2}` a number with
174    /// these decimals, and `{:dd/MM/yyyy HH:mm}` a date.
175    pub display_format: Option<String>,
176    /// For text: the mask it is typed through, where `0` is a digit, `A` a letter and `*` anything.
177    pub edit_mask: Option<String>,
178    /// For text: the values offered to pick from.
179    pub predefined: Vec<String>,
180    /// For an expression: the entity whose fields it can be about, which gives it a filter builder.
181    pub criteria_of: Option<String>,
182    /// It never leaves storage through a secured space, for anyone, and no screen shows it.
183    pub secret: bool,
184    /// What screen readers call it, when the caption is not enough. The caption by default.
185    pub accessible_name: Option<String>,
186    /// What screen readers say after its name, such as what goes in it. The hint by default.
187    pub accessible_description: Option<String>,
188}
189
190/// `opening_cents` as `Opening cents`.
191fn caption_of(name: &str) -> String {
192    let spaced = name.replace('_', " ");
193    let mut chars = spaced.chars();
194    chars.next().map_or_else(String::new, |first| first.to_uppercase().chain(chars).collect())
195}
196
197#[cfg_attr(feature = "live", ravenrs_live::expose)]
198impl FieldInfo {
199    /// A field shown in lists and on the form.
200    pub fn new(name: impl Into<String>, kind: FieldKind) -> Self {
201        let name = name.into();
202        // A list of options always shows one, so a new record has to hold it.
203        let default_value = match &kind {
204            FieldKind::Enum(options) => options.first().map_or(Value::Null, |o| Value::Text(o.code.clone())),
205            _ => Value::Null,
206        };
207        Self {
208            caption: caption_of(&name),
209            name,
210            in_list: !kind.is_tall() && !kind.is_collection(),
211            in_detail: true,
212            in_lookup: false,
213            is_key: false,
214            read_only: false,
215            calculated: None,
216            default_value,
217            editor: None,
218            placeholder: None,
219            wide: false,
220            autofocus: false,
221            hint: None,
222            lookup_filter: None,
223            none_caption: None,
224            back_reference: None,
225            non_cloneable: false,
226            aggregated: false,
227            most_bytes: None,
228            accepted: Vec::new(),
229            group: None,
230            display_format: None,
231            edit_mask: None,
232            predefined: Vec::new(),
233            criteria_of: None,
234            secret: false,
235            accessible_name: None,
236            accessible_description: None,
237            kind,
238        }
239    }
240
241    /// What people see.
242    pub fn caption(mut self, caption: impl Into<String>) -> Self {
243        self.caption = caption.into();
244        self
245    }
246
247    /// Makes it the key: read-only, and out of lists and forms unless told otherwise.
248    pub fn key(mut self) -> Self {
249        self.is_key = true;
250        self.read_only = true;
251        self.in_list = false;
252        self.in_detail = false;
253        self
254    }
255
256    /// Makes it the key, typed by people, as a code is.
257    pub fn typed_key(mut self) -> Self {
258        self.is_key = true;
259        self
260    }
261
262    /// Makes it what names a record, for [`EntityInfo::default_property`].
263    pub fn default_property(mut self) -> Self {
264        self.in_lookup = true;
265        self
266    }
267
268    /// It can be seen but not changed.
269    pub fn read_only(mut self) -> Self {
270        self.read_only = true;
271        self
272    }
273
274    /// Whether it has a column in lists.
275    pub fn in_list(mut self, show: bool) -> Self {
276        self.in_list = show;
277        self
278    }
279
280    /// Whether it is on the form.
281    pub fn in_detail(mut self, show: bool) -> Self {
282        self.in_detail = show;
283        self
284    }
285
286    /// Whether it has a column when picking a record.
287    pub fn in_lookup(mut self, show: bool) -> Self {
288        self.in_lookup = show;
289        self
290    }
291
292    /// Gets its value from this expression, read-only and not stored.
293    pub fn calculated(mut self, expression: impl Into<String>) -> Self {
294        self.calculated = Some(expression.into());
295        self.read_only = true;
296        self
297    }
298
299    /// The control that edits it, by name: `Segmented` for options side by side, or one
300    /// registered in an editor registry.
301    pub fn editor(mut self, editor: impl Into<String>) -> Self {
302        self.editor = Some(editor.into());
303        self
304    }
305
306    /// What an empty editor shows.
307    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
308        self.placeholder = Some(text.into());
309        self
310    }
311
312    /// Takes the whole width of its group on a form.
313    pub fn wide(mut self) -> Self {
314        self.wide = true;
315        self
316    }
317
318    /// Takes the keyboard when its form opens.
319    pub fn autofocus(mut self) -> Self {
320        self.autofocus = true;
321        self
322    }
323
324    /// What the form says under it.
325    pub fn hint(mut self, text: impl Into<String>) -> Self {
326        self.hint = Some(text.into());
327        self
328    }
329
330    /// What screen readers call it, instead of its caption: for a caption that only makes sense
331    /// next to what is around it, such as `Até` for the end of a period.
332    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
333        self.accessible_name = Some(name.into());
334        self
335    }
336
337    /// What screen readers say after its name, instead of its hint.
338    pub fn accessible_description(mut self, text: impl Into<String>) -> Self {
339        self.accessible_description = Some(text.into());
340        self
341    }
342
343    /// For a reference: what its records have to meet, where `?field` is the value of that field
344    /// of the record being edited, as in `[flow] = ?flow`.
345    pub fn lookup_filter(mut self, expression: impl Into<String>) -> Self {
346        self.lookup_filter = Some(expression.into());
347        self
348    }
349
350    /// For a reference that can be left empty and is picked from a list: what empty is called.
351    pub fn none_caption(mut self, caption: impl Into<String>) -> Self {
352        self.none_caption = Some(caption.into());
353        self
354    }
355
356    /// For a collection: the field of the other entity that points back here.
357    pub fn back_reference(mut self, field: impl Into<String>) -> Self {
358        self.back_reference = Some(field.into());
359        self
360    }
361
362    /// Stays behind when a record is cloned.
363    pub fn non_cloneable(mut self) -> Self {
364        self.non_cloneable = true;
365        self
366    }
367
368    /// For a collection: its records belong to this one, and are cloned with it.
369    pub fn aggregated(mut self) -> Self {
370        self.aggregated = true;
371        self
372    }
373
374    /// For a picture or a file: the largest taken, in bytes.
375    pub fn most_bytes(mut self, bytes: u64) -> Self {
376        self.most_bytes = Some(bytes);
377        self
378    }
379
380    /// For a file: the extensions offered, such as `pdf`.
381    pub fn accepted(mut self, extensions: impl IntoIterator<Item = impl Into<String>>) -> Self {
382        self.accepted = extensions.into_iter().map(Into::into).collect();
383        self
384    }
385
386    /// What a new record starts with.
387    pub fn default_value(mut self, value: impl Into<Value>) -> Self {
388        self.default_value = value.into();
389        self
390    }
391
392    /// Goes in the group with this title on a form, as the parts of one thing do.
393    pub fn group(mut self, title: impl Into<String>) -> Self {
394        self.group = Some(title.into());
395        self
396    }
397
398    /// How its value shows when it is not being edited: `{}` is the value, `{:.2}` a number with
399    /// these decimals, and `{:dd/MM/yyyy HH:mm}` a date.
400    pub fn display_format(mut self, format: impl Into<String>) -> Self {
401        self.display_format = Some(format.into());
402        self
403    }
404
405    /// For text: the mask it is typed through, where `0` is a digit, `A` a letter and `*` anything.
406    pub fn edit_mask(mut self, mask: impl Into<String>) -> Self {
407        self.edit_mask = Some(mask.into());
408        self
409    }
410
411    /// For text: the values offered to pick from.
412    pub fn predefined(mut self, values: impl IntoIterator<Item = impl Into<String>>) -> Self {
413        self.predefined = values.into_iter().map(Into::into).collect();
414        self
415    }
416
417    /// Keeps it from everyone: a secured space never gives it out or takes it in, and no screen
418    /// shows it, as with what is kept for a password.
419    pub fn secret(mut self) -> Self {
420        self.secret = true;
421        self.in_list = false;
422        self.in_detail = false;
423        self.in_lookup = false;
424        self
425    }
426
427    /// For an expression: the entity whose fields it can be about, which gives it a filter builder.
428    pub fn criteria_of(mut self, entity: impl Into<String>) -> Self {
429        self.criteria_of = Some(entity.into());
430        self
431    }
432
433    /// A field added in the model, under an entity's `Members`: `Kind` is that of a field with
434    /// plain values, with `Caption`, `Places` for decimals, `Options` for a list of options and
435    /// `Expression` for what is worked out and not stored.
436    fn from_model(entity: &str, node: &crate::ModelNode) -> Result<Self, ModelError> {
437        let wrong = |what: String| ModelError(format!("{entity}: the field {} {what}", node.id));
438        let options = || {
439            node.get("Options").map_or_else(Vec::new, |all| {
440                all.in_order()
441                    .iter()
442                    .map(|option| EnumOption::new(&option.id, option.text("Caption").unwrap_or(&option.id)))
443                    .collect()
444            })
445        };
446        let kind = match node.text("Kind").unwrap_or("Text") {
447            "Text" => FieldKind::Text,
448            "LongText" => FieldKind::LongText,
449            "Integer" => FieldKind::Integer,
450            "Decimal" => FieldKind::Decimal(node.integer("Places").unwrap_or(2).clamp(0, 12) as u8),
451            "Money" => FieldKind::Money,
452            "Date" => FieldKind::Date,
453            "DateTime" => FieldKind::DateTime,
454            "Boolean" => FieldKind::Boolean,
455            "Color" => FieldKind::Color,
456            "Enum" => FieldKind::Enum(options()),
457            "Tags" => FieldKind::Tags(options()),
458            other => return Err(wrong(format!("cannot be added in the model as {other}"))),
459        };
460        let mut field = Self::new(&node.id, kind);
461        if let Some(caption) = node.text("Caption") {
462            field.caption = caption.to_owned();
463        }
464        if let Some(expression) = node.text("Expression") {
465            field = field.calculated(expression);
466        }
467        Ok(field)
468    }
469
470    /// Whether the model added it: it comes with what it holds, which a declared one does not need.
471    pub(crate) fn is_custom(node: &crate::ModelNode, declared: &EntityInfo) -> bool {
472        declared.field_named(&node.id).is_none()
473    }
474}
475
476/// How a list gets its records.
477#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
478#[non_exhaustive]
479#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
480pub enum DataAccess {
481    /// All of them at once, kept in memory, so grouping and every search work on the spot.
482    #[default]
483    Client,
484    /// As they are needed, a chunk at a time, for lists too long to read at once.
485    Server,
486    /// All of them at once, but only what the columns show, and with no editing on the list.
487    DataView,
488}
489
490#[cfg_attr(feature = "live", ravenrs_live::expose)]
491impl DataAccess {
492    /// Its name in the model.
493    pub fn name(self) -> &'static str {
494        match self {
495            Self::Client => "Client",
496            Self::Server => "Server",
497            Self::DataView => "DataView",
498        }
499    }
500
501    /// From its name in the model; anything else is the usual.
502    pub fn from_name(name: &str) -> Self {
503        match name {
504            "Server" => Self::Server,
505            "DataView" => Self::DataView,
506            _ => Self::Client,
507        }
508    }
509}
510
511/// Something wrong in how the application was declared, in words for its developer.
512#[derive(Clone, Debug, PartialEq, Eq)]
513#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
514pub struct ModelError(pub String);
515
516impl fmt::Display for ModelError {
517    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518        f.write_str(&self.0)
519    }
520}
521
522impl std::error::Error for ModelError {}
523
524/// Another way to see the list of an entity, which people pick from a menu once there are two.
525#[derive(Clone, Debug, PartialEq, Eq)]
526#[non_exhaustive]
527#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
528pub struct ViewVariant {
529    /// What it is known by.
530    pub id: String,
531    /// What people see.
532    pub caption: String,
533    /// What its records have to meet; nothing means all of them.
534    pub filter: Option<String>,
535    /// The fields its columns show, in order; nothing means those of the list.
536    pub columns: Vec<String>,
537    /// The way it shows its records, by the name of a list editor; nothing means what the list uses.
538    pub editor: Option<String>,
539}
540
541#[cfg_attr(feature = "live", ravenrs_live::expose)]
542impl ViewVariant {
543    /// A variant that shows every record.
544    pub fn new(id: impl Into<String>, caption: impl Into<String>) -> Self {
545        Self { id: id.into(), caption: caption.into(), filter: None, columns: Vec::new(), editor: None }
546    }
547
548    /// Shows only the records that meet this expression.
549    pub fn filter(mut self, expression: impl Into<String>) -> Self {
550        self.filter = Some(expression.into());
551        self
552    }
553
554    /// Shows its records with the list editor of this name.
555    pub fn editor(mut self, editor: impl Into<String>) -> Self {
556        self.editor = Some(editor.into());
557        self
558    }
559
560    /// Shows these fields as its columns, in this order.
561    pub fn columns(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
562        self.columns = fields.into_iter().map(Into::into).collect();
563        self
564    }
565}
566
567/// A filter ready for people to pick from a menu on a list.
568#[derive(Clone, Debug, PartialEq, Eq)]
569#[non_exhaustive]
570#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
571pub struct ListFilter {
572    /// What it is known by.
573    pub id: String,
574    /// What people see.
575    pub caption: String,
576    /// What its records have to meet; nothing means all of them.
577    pub criteria: Option<String>,
578}
579
580#[cfg_attr(feature = "live", ravenrs_live::expose)]
581impl ListFilter {
582    /// A filter that lets everything through, as the choice that takes the others off.
583    pub fn new(id: impl Into<String>, caption: impl Into<String>) -> Self {
584        Self { id: id.into(), caption: caption.into(), criteria: None }
585    }
586
587    /// Shows only the records that meet this expression.
588    pub fn criteria(mut self, expression: impl Into<String>) -> Self {
589        self.criteria = Some(expression.into());
590        self
591    }
592}
593
594/// A kind of record.
595#[derive(Clone, Debug, PartialEq)]
596#[non_exhaustive]
597#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
598pub struct EntityInfo {
599    /// Its name, in the model and in code.
600    pub name: String,
601    /// What people see. Follows from the name unless given.
602    pub caption: String,
603    /// The group it goes under in the navigation; without one, it stays out.
604    pub navigation: Option<String>,
605    /// The name of its icon.
606    pub icon: Option<String>,
607    /// Its fields, in the order they were declared.
608    pub fields: Vec<FieldInfo>,
609    /// What its records have to meet.
610    pub rules: Vec<crate::Rule>,
611    /// How things look and behave by what its records hold.
612    pub appearance: Vec<crate::AppearanceRule>,
613    /// The ways to see its list.
614    pub variants: Vec<ViewVariant>,
615    /// What its list says it is there for, above the records.
616    pub description: Option<String>,
617    /// The states its records go through.
618    pub state_machines: Vec<crate::StateMachine>,
619    /// When to remind people of a record.
620    pub reminder: Option<crate::Reminder>,
621    /// Actions that set fields of the records selected.
622    pub actions: Vec<crate::RecordAction>,
623    /// What the list editors are told about this list: an editor's settings node, and in it names and values.
624    pub editor_settings: Vec<(String, String, String)>,
625    /// How many columns its form's fields go in; by how many there are, unless given.
626    pub form_columns: Option<u8>,
627    /// The filters people pick from on its list.
628    pub filters: Vec<ListFilter>,
629    /// What every record its list shows has to meet.
630    pub list_criteria: Option<String>,
631    /// Its records can be cloned.
632    pub cloneable: bool,
633    /// Its records are not kept by any storage: they live on a form or in a list given to them.
634    pub transient: bool,
635    /// A form says what is wrong with a field as it is left, and not only on saving.
636    pub checks_on_leave: bool,
637    /// People can change values right on its list.
638    pub list_editable: bool,
639    /// How its list gets its records.
640    pub data_access: DataAccess,
641    /// Places on its form for actions, by the name of their `ActionCategory::InForm`.
642    pub form_actions: Vec<String>,
643    /// The fields its form goes through first with the keyboard, in this order. The form shows
644    /// them in the same order, so the eye and the keyboard go the same way.
645    pub focus_order: Vec<String>,
646}
647
648#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
649impl EntityInfo {
650    /// An entity with no fields yet.
651    pub fn new(name: impl Into<String>) -> Self {
652        let name = name.into();
653        Self {
654            caption: caption_of(&name),
655            name,
656            navigation: None,
657            icon: None,
658            fields: Vec::new(),
659            rules: Vec::new(),
660            appearance: Vec::new(),
661            variants: Vec::new(),
662            description: None,
663            state_machines: Vec::new(),
664            actions: Vec::new(),
665            reminder: None,
666            editor_settings: Vec::new(),
667            form_columns: None,
668            filters: Vec::new(),
669            list_criteria: None,
670            cloneable: false,
671            transient: false,
672            checks_on_leave: false,
673            list_editable: false,
674            data_access: DataAccess::Client,
675            form_actions: Vec::new(),
676            focus_order: Vec::new(),
677        }
678    }
679
680    /// What people see.
681    pub fn caption(mut self, caption: impl Into<String>) -> Self {
682        self.caption = caption.into();
683        self
684    }
685
686    /// The group it goes under in the navigation.
687    pub fn navigation(mut self, group: impl Into<String>) -> Self {
688        self.navigation = Some(group.into());
689        self
690    }
691
692    /// The name of its icon.
693    pub fn icon(mut self, icon: impl Into<String>) -> Self {
694        self.icon = Some(icon.into());
695        self
696    }
697
698    /// Appends a field.
699    pub fn field(mut self, field: FieldInfo) -> Self {
700        self.fields.push(field);
701        self
702    }
703
704    /// Appends something its records have to meet.
705    pub fn rule(mut self, rule: crate::Rule) -> Self {
706        self.rules.push(rule);
707        self
708    }
709
710    /// Appends a rule about how things look.
711    pub fn appearance(mut self, rule: crate::AppearanceRule) -> Self {
712        self.appearance.push(rule);
713        self
714    }
715
716    /// Appends a filter people pick from on its list.
717    pub fn filter(mut self, filter: ListFilter) -> Self {
718        self.filters.push(filter);
719        self
720    }
721
722    /// What every record its list shows has to meet.
723    pub fn list_criteria(mut self, expression: impl Into<String>) -> Self {
724        self.list_criteria = Some(expression.into());
725        self
726    }
727
728    /// Its records can be cloned, with `CloneModule`.
729    pub fn cloneable(mut self) -> Self {
730        self.cloneable = true;
731        self
732    }
733
734    /// Its records are not kept by any storage: they live on a form or in a list given to them.
735    pub fn transient(mut self) -> Self {
736        self.transient = true;
737        self
738    }
739
740    /// A form says what is wrong with a field as it is left, and not only on saving.
741    pub fn checks_on_leave(mut self) -> Self {
742        self.checks_on_leave = true;
743        self
744    }
745
746    /// People can change values right on its list, with the rules checked as they do.
747    pub fn list_editable(mut self) -> Self {
748        self.list_editable = true;
749        self
750    }
751
752    /// How its list gets its records.
753    pub fn data_access(mut self, access: DataAccess) -> Self {
754        self.data_access = access;
755        self
756    }
757
758    /// A place on its form for the actions of `ActionCategory::InForm` with this name.
759    pub fn form_actions(mut self, name: impl Into<String>) -> Self {
760        self.form_actions.push(name.into());
761        self
762    }
763
764    /// The fields its form goes through first with the keyboard, in this order; the others follow
765    /// in the order they were declared. The form shows them in the same order.
766    pub fn focus_order(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
767        self.focus_order = fields.into_iter().map(Into::into).collect();
768        self
769    }
770
771    /// How many columns its form's fields go in.
772    pub fn form_columns(mut self, columns: u8) -> Self {
773        self.form_columns = Some(columns);
774        self
775    }
776
777    /// What its list says it is there for, above the records.
778    pub fn description(mut self, text: impl Into<String>) -> Self {
779        self.description = Some(text.into());
780        self
781    }
782
783    /// Gives its records states, and only these ways between them. Its field becomes read-only on
784    /// forms and starts in the machine's first state, its actions come by themselves, and any other
785    /// change is refused when it is kept. Declare the field first.
786    pub fn state_machine(mut self, machine: crate::StateMachine) -> Self {
787        if let Some(field) = self.fields.iter_mut().find(|field| field.name == machine.field) {
788            field.read_only = true;
789            if let Some(start) = &machine.start {
790                field.default_value = Value::Text(start.clone());
791            }
792        }
793        self.state_machines.push(machine);
794        self
795    }
796
797    /// An appearance that holds while its field is in this state.
798    pub fn state_appearance(self, field: &str, state: &str, rule: crate::AppearanceRule) -> Self {
799        let state = state.replace('\'', "''");
800        self.appearance(rule.criteria(format!("[{field}] = '{state}'")))
801    }
802
803    /// An action that sets fields of the records selected. It comes by itself on its list and
804    /// its form.
805    pub fn action(mut self, action: crate::RecordAction) -> Self {
806        self.actions.push(action);
807        self
808    }
809
810    /// Reminds people of its records.
811    pub fn reminder(mut self, reminder: crate::Reminder) -> Self {
812        self.reminder = Some(reminder);
813        self
814    }
815
816    /// Tells a list editor something about its list, as `ChartSettings`, `Kind` and `Line`. It goes
817    /// in the model under the list's view, so a person can change it like any other setting.
818    pub fn editor_setting(
819        mut self,
820        node: impl Into<String>,
821        name: impl Into<String>,
822        value: impl Into<String>,
823    ) -> Self {
824        self.editor_settings.push((node.into(), name.into(), value.into()));
825        self
826    }
827
828    /// Appends a way to see its list.
829    pub fn variant(mut self, variant: ViewVariant) -> Self {
830        self.variants.push(variant);
831        self
832    }
833
834    /// A field by name.
835    pub fn field_named(&self, name: &str) -> Option<&FieldInfo> {
836        self.fields.iter().find(|f| f.name == name)
837    }
838
839    /// The field that identifies a record.
840    pub fn key_field(&self) -> Option<&FieldInfo> {
841        self.fields.iter().find(|f| f.is_key)
842    }
843
844    /// The field that names a record: the first in lookups, or else the first with text.
845    pub fn default_property(&self) -> Option<&FieldInfo> {
846        self.fields
847            .iter()
848            .find(|f| f.in_lookup)
849            .or_else(|| self.fields.iter().find(|f| matches!(f.kind, FieldKind::Text) && !f.is_key))
850    }
851
852    /// What expressions over this entity may refer to.
853    pub fn schema(&self) -> Schema {
854        self.fields
855            .iter()
856            .filter(|f| !f.kind.is_collection())
857            .fold(Schema::new(), |schema, f| schema.field(&f.name, f.kind.field_type()))
858    }
859
860    /// Says what is wrong with how it was declared.
861    pub fn check(&self) -> Result<(), ModelError> {
862        let wrong = |what: String| Err(ModelError(format!("{}: {what}", self.name)));
863        if self.name.trim().is_empty() {
864            return Err(ModelError("an entity has no name".into()));
865        }
866        match self.fields.iter().filter(|f| f.is_key).count() {
867            1 => {}
868            0 => return wrong("it has no key field".into()),
869            n => return wrong(format!("it has {n} key fields, and only one can be")),
870        }
871        for (i, field) in self.fields.iter().enumerate() {
872            if self.fields[..i].iter().any(|f| f.name == field.name) {
873                return wrong(format!("the field {} is declared twice", field.name));
874            }
875            if let Some(expression) = &field.calculated {
876                let parsed = Criteria::parse(expression)
877                    .map_err(|e| ModelError(format!("{}.{}: {e}", self.name, field.name)))?;
878                if let Some(problem) = ravenrs_criteria::validate(&parsed, &self.schema()).first() {
879                    return wrong(format!("the field {}: {problem}", field.name));
880                }
881            }
882        }
883        for field in &self.focus_order {
884            if self.field_named(field).is_none() {
885                return wrong(format!("the focus order names {field}, which is not a field"));
886            }
887        }
888        for action in &self.actions {
889            action.check(self).map_err(|e| ModelError(format!("{}: {}", self.name, e.0)))?;
890        }
891        for machine in &self.state_machines {
892            if self.field_named(&machine.field).is_none() {
893                return wrong(format!("a state machine is over {}, which is not a field", machine.field));
894            }
895            if let Some(problem) = machine.problem() {
896                return wrong(problem);
897            }
898        }
899        if let Some(reminder) = &self.reminder
900            && !self
901                .field_named(&reminder.field)
902                .is_some_and(|field| matches!(field.kind, FieldKind::Date | FieldKind::DateTime))
903        {
904            return wrong(format!("a reminder needs a date in {}", reminder.field));
905        }
906        let check_expression = |what: String, expression: &str| -> Result<(), ModelError> {
907            let parsed =
908                Criteria::parse(expression).map_err(|e| ModelError(format!("{}, {what}: {e}", self.name)))?;
909            // A function the application added is known only where it runs.
910            let environment = crate::environment();
911            let problems = ravenrs_criteria::validate(&parsed, &self.schema());
912            let found = problems.iter().find(|problem| {
913                !matches!(problem, ravenrs_criteria::Problem::UnknownFunction(name) if environment.has_function(name))
914            });
915            match found {
916                Some(problem) => Err(ModelError(format!("{}: {what}: {problem}", self.name))),
917                None => Ok(()),
918            }
919        };
920        for filter in &self.filters {
921            if let Some(expression) = &filter.criteria {
922                check_expression(format!("the filter {}", filter.id), expression)?;
923            }
924        }
925        if let Some(expression) = &self.list_criteria {
926            check_expression("the list's criteria".to_owned(), expression)?;
927        }
928        for variant in &self.variants {
929            if let Some(gone) = variant.columns.iter().find(|name| self.field_named(name).is_none()) {
930                return wrong(format!("the variant {} shows {gone}, which is not a field", variant.id));
931            }
932            if let Some(expression) = &variant.filter {
933                let parsed = Criteria::parse(expression)
934                    .map_err(|e| ModelError(format!("{}, variant {}: {e}", self.name, variant.id)))?;
935                if let Some(problem) = ravenrs_criteria::validate(&parsed, &self.schema()).first() {
936                    return wrong(format!("the variant {}: {problem}", variant.id));
937                }
938            }
939        }
940        for field in &self.fields {
941            if let Some(expression) = &field.lookup_filter {
942                Criteria::parse(expression)
943                    .map_err(|e| ModelError(format!("{}.{}: {e}", self.name, field.name)))?;
944            }
945        }
946        for rule in &self.rules {
947            rule.check(self)?;
948        }
949        for rule in &self.appearance {
950            rule.check(self)?;
951        }
952        Ok(())
953    }
954
955    /// A record of this entity with what a new one starts with.
956    pub fn new_object(&self) -> Object {
957        let values = self
958            .fields
959            .iter()
960            .filter(|f| f.calculated.is_none() && !f.kind.is_collection())
961            .map(|f| (f.name.clone(), f.default_value.clone()))
962            .collect();
963        let mut object = Object { entity: self.name.clone(), values };
964        for field in self.fields.iter().filter(|field| matches!(field.kind, FieldKind::Guid)) {
965            if matches!(object.get(&field.name), Value::Null) {
966                object.set(&field.name, uuid::Uuid::new_v4().to_string());
967            }
968        }
969        object
970    }
971
972    /// This entity as the model has it now: what people see and the rules it added.
973    pub(crate) fn as_in_model(&self, root: &crate::ModelNode) -> Result<Self, ModelError> {
974        let mut entity = self.clone();
975        let Some(node) = root.at(&["Entities", &self.name]) else { return Ok(entity) };
976        if let Some(members) = node.get("Members") {
977            for member in members.children.iter().filter(|member| FieldInfo::is_custom(member, self)) {
978                entity.fields.push(FieldInfo::from_model(&self.name, member)?);
979            }
980        }
981        if let Some(caption) = node.text("Caption") {
982            entity.caption = caption.to_owned();
983        }
984        for field in &mut entity.fields {
985            let Some(member) = node.at(&["Members", &field.name]) else { continue };
986            if let Some(caption) = member.text("Caption") {
987                field.caption = caption.to_owned();
988            }
989            if let FieldKind::Enum(options) = &mut field.kind {
990                for option in options {
991                    let found = member.at(&["Options", &option.code]).and_then(|n| n.text("Caption"));
992                    if let Some(caption) = found {
993                        option.caption = caption.to_owned();
994                    }
995                }
996            }
997        }
998        if let Some(rules) = node.get("Rules") {
999            for rule in rules.in_order() {
1000                entity.rules.push(crate::Rule::from_model(rule)?);
1001            }
1002        }
1003        entity.check()?;
1004        Ok(entity)
1005    }
1006
1007    /// Fills in the calculated fields, in the order declared, so one can read another.
1008    pub fn compute(&self, object: &mut Object, environment: &Environment) {
1009        for field in &self.fields {
1010            let Some(expression) = &field.calculated else { continue };
1011            // `check` already refused what does not parse.
1012            let value = Criteria::parse(expression)
1013                .ok()
1014                .and_then(|parsed| Scope::new(environment).row(&*object).evaluate(&parsed).ok())
1015                .unwrap_or(Value::Null);
1016            object.values.insert(field.name.clone(), value);
1017        }
1018    }
1019
1020    /// The object as it is stored, with no calculated values.
1021    pub fn strip_calculated(&self, object: &mut Object) {
1022        for field in self.fields.iter().filter(|f| f.calculated.is_some()) {
1023            object.values.remove(&field.name);
1024        }
1025    }
1026}
1027
1028/// A record: its entity's name and a value per field. Dynamic, so filters, rules and
1029/// permissions read every entity the same way.
1030#[derive(Clone, Debug, PartialEq)]
1031#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1032pub struct Object {
1033    entity: String,
1034    values: BTreeMap<String, Value>,
1035}
1036
1037#[cfg_attr(feature = "live", ravenrs_live::expose)]
1038impl Object {
1039    /// A record of this entity with these values, as storage hands them over.
1040    pub fn from_values(entity: impl Into<String>, values: BTreeMap<String, Value>) -> Self {
1041        Self { entity: entity.into(), values }
1042    }
1043
1044    /// The name of its entity.
1045    pub fn entity(&self) -> &str {
1046        &self.entity
1047    }
1048
1049    /// The value of a field; nothing is `Null`.
1050    pub fn get(&self, field: &str) -> Value {
1051        self.values.get(field).cloned().unwrap_or(Value::Null)
1052    }
1053
1054    /// Changes the value of a field.
1055    pub fn set(&mut self, field: &str, value: impl Into<Value>) {
1056        self.values.insert(field.to_owned(), value.into());
1057    }
1058
1059    /// Every value, by field.
1060    pub fn values(&self) -> &BTreeMap<String, Value> {
1061        &self.values
1062    }
1063
1064    /// Leaves only these fields.
1065    pub fn keep_only(&mut self, fields: &[&str]) {
1066        self.values.retain(|name, _| fields.contains(&name.as_str()));
1067    }
1068}
1069
1070impl Record for Object {
1071    fn field(&self, path: &[String]) -> Option<Value> {
1072        match path {
1073            [name] => self.values.get(name).cloned(),
1074            _ => None,
1075        }
1076    }
1077
1078    fn collection(&self, _path: &[String]) -> Option<Vec<&Self>> {
1079        None
1080    }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086
1087    #[test]
1088    fn captions_follow_from_names() {
1089        assert_eq!(caption_of("opening_cents"), "Opening cents");
1090        assert_eq!(FieldInfo::new("name", FieldKind::Text).caption, "Name");
1091    }
1092
1093    #[test]
1094    fn a_declaration_says_what_is_wrong_with_it() {
1095        let key = || FieldInfo::new("id", FieldKind::Integer).key();
1096        let name = |e: &EntityInfo| e.check().map_err(|e| e.0);
1097        assert!(name(&EntityInfo::new("A")).is_err_and(|e| e.contains("no key")));
1098        let two = EntityInfo::new("A").field(key()).field(FieldInfo::new("b", FieldKind::Text).typed_key());
1099        assert!(name(&two).is_err_and(|e| e.contains("2 key fields")));
1100        let twice = EntityInfo::new("A").field(key()).field(FieldInfo::new("id", FieldKind::Text));
1101        assert!(name(&twice).is_err_and(|e| e.contains("twice")));
1102        let bad = EntityInfo::new("A")
1103            .field(key())
1104            .field(FieldInfo::new("c", FieldKind::Integer).calculated("[nope] + 1"));
1105        assert!(name(&bad).is_err_and(|e| e.contains("A") && e.contains("c")));
1106        assert!(EntityInfo::new("A").field(key()).check().is_ok());
1107    }
1108
1109    #[test]
1110    fn the_default_property_is_the_one_named_or_the_first_text() {
1111        let plain = EntityInfo::new("A")
1112            .field(FieldInfo::new("code", FieldKind::Text).typed_key())
1113            .field(FieldInfo::new("title", FieldKind::Text));
1114        assert_eq!(plain.default_property().map(|f| f.name.as_str()), Some("title"));
1115        let named = plain.field(FieldInfo::new("other", FieldKind::Text).default_property());
1116        assert_eq!(named.default_property().map(|f| f.name.as_str()), Some("other"));
1117    }
1118
1119    #[test]
1120    fn the_schema_answers_for_the_fields() {
1121        let entity = EntityInfo::new("A")
1122            .field(FieldInfo::new("id", FieldKind::Integer).key())
1123            .field(FieldInfo::new("name", FieldKind::Text));
1124        let good = Criteria::parse("[name] = 'x' And [id] > 1").expect("parses");
1125        assert!(ravenrs_criteria::validate(&good, &entity.schema()).is_empty());
1126        let bad = Criteria::parse("[nope] = 1").expect("parses");
1127        assert!(!ravenrs_criteria::validate(&bad, &entity.schema()).is_empty());
1128    }
1129}