Skip to main content

ravenrs_app/
object.rs

1//! Records as Rust types.
2
3use ravenrs_criteria::{DateTime, Value};
4
5use crate::{EntityInfo, FieldKind, Object, ObjectSpace, Query, SpaceError};
6
7/// A Rust type a field can have.
8pub trait FieldValue: Sized {
9    /// What it holds, unless the field's attribute says otherwise.
10    fn kind() -> FieldKind;
11    /// As a record holds it.
12    fn to_value(&self) -> Value;
13    /// From what a record holds. What cannot be read gives the type's empty value.
14    fn from_value(value: Value) -> Self;
15}
16
17impl FieldValue for String {
18    fn kind() -> FieldKind {
19        FieldKind::Text
20    }
21    fn to_value(&self) -> Value {
22        Value::Text(self.clone())
23    }
24    fn from_value(value: Value) -> Self {
25        if let Value::Text(text) = value { text } else { String::new() }
26    }
27}
28
29impl FieldValue for bool {
30    fn kind() -> FieldKind {
31        FieldKind::Boolean
32    }
33    fn to_value(&self) -> Value {
34        Value::Bool(*self)
35    }
36    fn from_value(value: Value) -> Self {
37        matches!(value, Value::Bool(true))
38    }
39}
40
41macro_rules! whole_numbers {
42    ($($ty:ty),*) => {$(
43        impl FieldValue for $ty {
44            fn kind() -> FieldKind {
45                FieldKind::Integer
46            }
47            fn to_value(&self) -> Value {
48                Value::Number(*self as f64)
49            }
50            fn from_value(value: Value) -> Self {
51                if let Value::Number(number) = value { number.round() as $ty } else { 0 }
52            }
53        }
54    )*};
55}
56
57whole_numbers!(i32, i64, u32, u64, usize);
58
59impl FieldValue for f64 {
60    fn kind() -> FieldKind {
61        FieldKind::Decimal(2)
62    }
63    fn to_value(&self) -> Value {
64        Value::Number(*self)
65    }
66    fn from_value(value: Value) -> Self {
67        if let Value::Number(number) = value { number } else { 0.0 }
68    }
69}
70
71impl FieldValue for DateTime {
72    fn kind() -> FieldKind {
73        FieldKind::DateTime
74    }
75    fn to_value(&self) -> Value {
76        Value::Date(*self)
77    }
78    fn from_value(value: Value) -> Self {
79        if let Value::Date(moment) = value { moment } else { DateTime { days: 0, seconds: 0 } }
80    }
81}
82
83impl<T: FieldValue> FieldValue for Option<T> {
84    fn kind() -> FieldKind {
85        T::kind()
86    }
87    fn to_value(&self) -> Value {
88        self.as_ref().map_or(Value::Null, T::to_value)
89    }
90    fn from_value(value: Value) -> Self {
91        if matches!(value, Value::Null) { None } else { Some(T::from_value(value)) }
92    }
93}
94
95/// A record as a Rust struct. Derive it:
96///
97/// ```
98/// use ravenrs_app::{BusinessEnum, BusinessObject};
99///
100/// #[derive(BusinessEnum, Clone, Debug, PartialEq)]
101/// enum Kind {
102///     #[option(caption = "Corrente")]
103///     Checking,
104///     #[option(caption = "Poupança")]
105///     Savings,
106/// }
107///
108/// #[derive(BusinessObject, Clone, Debug, PartialEq)]
109/// #[object(caption = "Contas", navigation = "Cadastros", icon = "wallet")]
110/// struct Account {
111///     #[field(key)]
112///     id: i64,
113///     #[field(caption = "Nome", default_property)]
114///     name: String,
115///     #[field(caption = "Tipo")]
116///     kind: Kind,
117///     #[field(caption = "Saldo inicial", money)]
118///     opening: i64,
119/// }
120///
121/// let entity = Account::entity_info();
122/// assert_eq!(entity.caption, "Contas");
123/// assert!(entity.check().is_ok());
124/// let account = Account { id: 1, name: "Carteira".into(), kind: Kind::Savings, opening: 1250 };
125/// assert_eq!(Account::from_object(&account.to_object()), account);
126/// ```
127pub trait BusinessObject: Sized {
128    /// What the record is.
129    fn entity_info() -> EntityInfo;
130    /// As a dynamic record.
131    fn to_object(&self) -> Object;
132    /// From a dynamic record.
133    fn from_object(object: &Object) -> Self;
134}
135
136/// Reading and writing with Rust structs, on any object space.
137pub trait ObjectSpaceExt {
138    /// Marks this as a new record. A numbered key left at zero gets the next number.
139    fn add<T: BusinessObject>(&mut self, record: &T) -> Result<(), SpaceError>;
140    /// Reads records.
141    fn read<T: BusinessObject>(&self, query: &Query) -> Result<Vec<T>, SpaceError>;
142}
143
144impl<S: ObjectSpace + ?Sized> ObjectSpaceExt for S {
145    fn add<T: BusinessObject>(&mut self, record: &T) -> Result<(), SpaceError> {
146        let entity = T::entity_info();
147        let mut object = self.new_object(&entity.name)?;
148        let given = record.to_object();
149        for (name, value) in given.values() {
150            let numbered_key =
151                entity.field_named(name).is_some_and(|f| f.is_key && matches!(f.kind, FieldKind::Integer))
152                    && matches!(value, Value::Number(n) if *n == 0.0);
153            if !numbered_key {
154                object.set(name, value.clone());
155            }
156        }
157        self.insert(object)
158    }
159
160    fn read<T: BusinessObject>(&self, query: &Query) -> Result<Vec<T>, SpaceError> {
161        let found = self.query(&T::entity_info().name, query)?;
162        Ok(found.iter().map(T::from_object).collect())
163    }
164}