1use std::collections::BTreeMap;
4use std::fmt;
5
6use ravenrs_criteria::{Criteria, Environment, FieldType, Record, Schema, Scope, Value};
7
8#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
12pub enum FieldKind {
13 Text,
15 LongText,
17 Integer,
19 Decimal(u8),
21 Money,
23 Date,
25 DateTime,
27 Boolean,
29 Enum(Vec<EnumOption>),
31 Reference(String),
33 Collection(String),
35 Color,
37 Image,
39 File,
41 Criteria,
43 Guid,
45 Tags(Vec<EnumOption>),
47}
48
49#[cfg_attr(feature = "live", ravenrs_live::expose)]
50impl FieldKind {
51 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 pub(crate) fn is_tall(&self) -> bool {
70 matches!(self, Self::LongText | Self::Image | Self::File | Self::Criteria)
71 }
72
73 pub(crate) fn is_collection(&self) -> bool {
75 matches!(self, Self::Collection(_))
76 }
77
78 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#[derive(Clone, Debug, PartialEq, Eq)]
104#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
105pub struct EnumOption {
106 pub code: String,
108 pub caption: String,
110}
111
112#[cfg_attr(feature = "live", ravenrs_live::expose)]
113impl EnumOption {
114 pub fn new(code: impl Into<String>, caption: impl Into<String>) -> Self {
116 Self { code: code.into(), caption: caption.into() }
117 }
118}
119
120#[derive(Clone, Debug, PartialEq)]
122#[non_exhaustive]
123#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
124pub struct FieldInfo {
125 pub name: String,
127 pub caption: String,
129 pub kind: FieldKind,
131 pub is_key: bool,
133 pub read_only: bool,
135 pub in_list: bool,
137 pub in_detail: bool,
139 pub in_lookup: bool,
141 pub calculated: Option<String>,
143 pub default_value: Value,
145 pub editor: Option<String>,
147 pub placeholder: Option<String>,
149 pub wide: bool,
151 pub autofocus: bool,
153 pub hint: Option<String>,
155 pub lookup_filter: Option<String>,
158 pub none_caption: Option<String>,
160 pub back_reference: Option<String>,
163 pub non_cloneable: bool,
165 pub aggregated: bool,
167 pub most_bytes: Option<u64>,
169 pub accepted: Vec<String>,
171 pub group: Option<String>,
173 pub display_format: Option<String>,
176 pub edit_mask: Option<String>,
178 pub predefined: Vec<String>,
180 pub criteria_of: Option<String>,
182 pub secret: bool,
184 pub accessible_name: Option<String>,
186 pub accessible_description: Option<String>,
188}
189
190fn 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 pub fn new(name: impl Into<String>, kind: FieldKind) -> Self {
201 let name = name.into();
202 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 pub fn caption(mut self, caption: impl Into<String>) -> Self {
243 self.caption = caption.into();
244 self
245 }
246
247 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 pub fn typed_key(mut self) -> Self {
258 self.is_key = true;
259 self
260 }
261
262 pub fn default_property(mut self) -> Self {
264 self.in_lookup = true;
265 self
266 }
267
268 pub fn read_only(mut self) -> Self {
270 self.read_only = true;
271 self
272 }
273
274 pub fn in_list(mut self, show: bool) -> Self {
276 self.in_list = show;
277 self
278 }
279
280 pub fn in_detail(mut self, show: bool) -> Self {
282 self.in_detail = show;
283 self
284 }
285
286 pub fn in_lookup(mut self, show: bool) -> Self {
288 self.in_lookup = show;
289 self
290 }
291
292 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 pub fn editor(mut self, editor: impl Into<String>) -> Self {
302 self.editor = Some(editor.into());
303 self
304 }
305
306 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
308 self.placeholder = Some(text.into());
309 self
310 }
311
312 pub fn wide(mut self) -> Self {
314 self.wide = true;
315 self
316 }
317
318 pub fn autofocus(mut self) -> Self {
320 self.autofocus = true;
321 self
322 }
323
324 pub fn hint(mut self, text: impl Into<String>) -> Self {
326 self.hint = Some(text.into());
327 self
328 }
329
330 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
333 self.accessible_name = Some(name.into());
334 self
335 }
336
337 pub fn accessible_description(mut self, text: impl Into<String>) -> Self {
339 self.accessible_description = Some(text.into());
340 self
341 }
342
343 pub fn lookup_filter(mut self, expression: impl Into<String>) -> Self {
346 self.lookup_filter = Some(expression.into());
347 self
348 }
349
350 pub fn none_caption(mut self, caption: impl Into<String>) -> Self {
352 self.none_caption = Some(caption.into());
353 self
354 }
355
356 pub fn back_reference(mut self, field: impl Into<String>) -> Self {
358 self.back_reference = Some(field.into());
359 self
360 }
361
362 pub fn non_cloneable(mut self) -> Self {
364 self.non_cloneable = true;
365 self
366 }
367
368 pub fn aggregated(mut self) -> Self {
370 self.aggregated = true;
371 self
372 }
373
374 pub fn most_bytes(mut self, bytes: u64) -> Self {
376 self.most_bytes = Some(bytes);
377 self
378 }
379
380 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 pub fn default_value(mut self, value: impl Into<Value>) -> Self {
388 self.default_value = value.into();
389 self
390 }
391
392 pub fn group(mut self, title: impl Into<String>) -> Self {
394 self.group = Some(title.into());
395 self
396 }
397
398 pub fn display_format(mut self, format: impl Into<String>) -> Self {
401 self.display_format = Some(format.into());
402 self
403 }
404
405 pub fn edit_mask(mut self, mask: impl Into<String>) -> Self {
407 self.edit_mask = Some(mask.into());
408 self
409 }
410
411 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 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 pub fn criteria_of(mut self, entity: impl Into<String>) -> Self {
429 self.criteria_of = Some(entity.into());
430 self
431 }
432
433 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 pub(crate) fn is_custom(node: &crate::ModelNode, declared: &EntityInfo) -> bool {
472 declared.field_named(&node.id).is_none()
473 }
474}
475
476#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
478#[non_exhaustive]
479#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
480pub enum DataAccess {
481 #[default]
483 Client,
484 Server,
486 DataView,
488}
489
490#[cfg_attr(feature = "live", ravenrs_live::expose)]
491impl DataAccess {
492 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 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#[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#[derive(Clone, Debug, PartialEq, Eq)]
526#[non_exhaustive]
527#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
528pub struct ViewVariant {
529 pub id: String,
531 pub caption: String,
533 pub filter: Option<String>,
535 pub columns: Vec<String>,
537 pub editor: Option<String>,
539}
540
541#[cfg_attr(feature = "live", ravenrs_live::expose)]
542impl ViewVariant {
543 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 pub fn filter(mut self, expression: impl Into<String>) -> Self {
550 self.filter = Some(expression.into());
551 self
552 }
553
554 pub fn editor(mut self, editor: impl Into<String>) -> Self {
556 self.editor = Some(editor.into());
557 self
558 }
559
560 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#[derive(Clone, Debug, PartialEq, Eq)]
569#[non_exhaustive]
570#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
571pub struct ListFilter {
572 pub id: String,
574 pub caption: String,
576 pub criteria: Option<String>,
578}
579
580#[cfg_attr(feature = "live", ravenrs_live::expose)]
581impl ListFilter {
582 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 pub fn criteria(mut self, expression: impl Into<String>) -> Self {
589 self.criteria = Some(expression.into());
590 self
591 }
592}
593
594#[derive(Clone, Debug, PartialEq)]
596#[non_exhaustive]
597#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
598pub struct EntityInfo {
599 pub name: String,
601 pub caption: String,
603 pub navigation: Option<String>,
605 pub icon: Option<String>,
607 pub fields: Vec<FieldInfo>,
609 pub rules: Vec<crate::Rule>,
611 pub appearance: Vec<crate::AppearanceRule>,
613 pub variants: Vec<ViewVariant>,
615 pub description: Option<String>,
617 pub state_machines: Vec<crate::StateMachine>,
619 pub reminder: Option<crate::Reminder>,
621 pub actions: Vec<crate::RecordAction>,
623 pub editor_settings: Vec<(String, String, String)>,
625 pub form_columns: Option<u8>,
627 pub filters: Vec<ListFilter>,
629 pub list_criteria: Option<String>,
631 pub cloneable: bool,
633 pub transient: bool,
635 pub checks_on_leave: bool,
637 pub list_editable: bool,
639 pub data_access: DataAccess,
641 pub form_actions: Vec<String>,
643 pub focus_order: Vec<String>,
646}
647
648#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
649impl EntityInfo {
650 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 pub fn caption(mut self, caption: impl Into<String>) -> Self {
682 self.caption = caption.into();
683 self
684 }
685
686 pub fn navigation(mut self, group: impl Into<String>) -> Self {
688 self.navigation = Some(group.into());
689 self
690 }
691
692 pub fn icon(mut self, icon: impl Into<String>) -> Self {
694 self.icon = Some(icon.into());
695 self
696 }
697
698 pub fn field(mut self, field: FieldInfo) -> Self {
700 self.fields.push(field);
701 self
702 }
703
704 pub fn rule(mut self, rule: crate::Rule) -> Self {
706 self.rules.push(rule);
707 self
708 }
709
710 pub fn appearance(mut self, rule: crate::AppearanceRule) -> Self {
712 self.appearance.push(rule);
713 self
714 }
715
716 pub fn filter(mut self, filter: ListFilter) -> Self {
718 self.filters.push(filter);
719 self
720 }
721
722 pub fn list_criteria(mut self, expression: impl Into<String>) -> Self {
724 self.list_criteria = Some(expression.into());
725 self
726 }
727
728 pub fn cloneable(mut self) -> Self {
730 self.cloneable = true;
731 self
732 }
733
734 pub fn transient(mut self) -> Self {
736 self.transient = true;
737 self
738 }
739
740 pub fn checks_on_leave(mut self) -> Self {
742 self.checks_on_leave = true;
743 self
744 }
745
746 pub fn list_editable(mut self) -> Self {
748 self.list_editable = true;
749 self
750 }
751
752 pub fn data_access(mut self, access: DataAccess) -> Self {
754 self.data_access = access;
755 self
756 }
757
758 pub fn form_actions(mut self, name: impl Into<String>) -> Self {
760 self.form_actions.push(name.into());
761 self
762 }
763
764 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 pub fn form_columns(mut self, columns: u8) -> Self {
773 self.form_columns = Some(columns);
774 self
775 }
776
777 pub fn description(mut self, text: impl Into<String>) -> Self {
779 self.description = Some(text.into());
780 self
781 }
782
783 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 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 pub fn action(mut self, action: crate::RecordAction) -> Self {
806 self.actions.push(action);
807 self
808 }
809
810 pub fn reminder(mut self, reminder: crate::Reminder) -> Self {
812 self.reminder = Some(reminder);
813 self
814 }
815
816 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 pub fn variant(mut self, variant: ViewVariant) -> Self {
830 self.variants.push(variant);
831 self
832 }
833
834 pub fn field_named(&self, name: &str) -> Option<&FieldInfo> {
836 self.fields.iter().find(|f| f.name == name)
837 }
838
839 pub fn key_field(&self) -> Option<&FieldInfo> {
841 self.fields.iter().find(|f| f.is_key)
842 }
843
844 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 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 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 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 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 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 pub fn compute(&self, object: &mut Object, environment: &Environment) {
1009 for field in &self.fields {
1010 let Some(expression) = &field.calculated else { continue };
1011 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 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#[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 pub fn from_values(entity: impl Into<String>, values: BTreeMap<String, Value>) -> Self {
1041 Self { entity: entity.into(), values }
1042 }
1043
1044 pub fn entity(&self) -> &str {
1046 &self.entity
1047 }
1048
1049 pub fn get(&self, field: &str) -> Value {
1051 self.values.get(field).cloned().unwrap_or(Value::Null)
1052 }
1053
1054 pub fn set(&mut self, field: &str, value: impl Into<Value>) {
1056 self.values.insert(field.to_owned(), value.into());
1057 }
1058
1059 pub fn values(&self) -> &BTreeMap<String, Value> {
1061 &self.values
1062 }
1063
1064 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}