Skip to main content

ravenrs_app_library/
lib.rs

1//! Records most business applications need, ready to use or to extend: every one is a plain
2//! [`EntityInfo`], so adding a field or a rule is a matter of calling it before handing it over.
3//!
4//! ```
5//! use ravenrs_app::{Application, FieldInfo, FieldKind};
6//! use ravenrs_app_library::{LibraryModule, person};
7//!
8//! let application = Application::builder("Contacts")
9//!     .module(LibraryModule::new().task().entity(person().field(FieldInfo::new("badge", FieldKind::Text))))
10//!     .build()?;
11//! // What they point to comes along.
12//! assert!(application.entities().iter().any(|entity| entity.name == "Address"));
13//! # Ok::<(), ravenrs_app::ModelError>(())
14//! ```
15
16use ravenrs_app::{
17    Comparison, EntityInfo, EnumOption, FieldInfo, FieldKind, Module, Operand, Rule, RuleKind,
18};
19
20/// What an email address looks like, loosely, so it only catches slips.
21const EMAIL: &str = r"^[^@\s]+@[^@\s]+\.[^@\s]+$";
22
23/// Their words in Brazilian Portuguese, as a difference for `translation`.
24pub const PT_BR: &str = include_str!("pt_br.json");
25
26fn key() -> FieldInfo {
27    FieldInfo::new("id", FieldKind::Integer).key()
28}
29
30fn text(name: &str) -> FieldInfo {
31    FieldInfo::new(name, FieldKind::Text)
32}
33
34fn email() -> FieldInfo {
35    text("email").caption("Email")
36}
37
38fn reference(name: &str, target: &str) -> FieldInfo {
39    FieldInfo::new(name, FieldKind::Reference(target.to_owned()))
40}
41
42/// A country, which addresses point to.
43pub fn country() -> EntityInfo {
44    EntityInfo::new("Country")
45        .caption("Countries")
46        .icon("globe")
47        .field(key())
48        .field(text("name").default_property())
49        .field(text("code").hint("ISO 3166, such as BR"))
50        .rule(Rule::required("name"))
51        .rule(Rule::unique("name"))
52}
53
54/// A postal address.
55pub fn address() -> EntityInfo {
56    EntityInfo::new("Address")
57        .caption("Addresses")
58        .icon("map-pin")
59        .field(key())
60        .field(
61            text("full_address")
62                .caption("Address")
63                .calculated("Concat([street], ', ', [number], ' - ', [city], ' ', [state])")
64                .default_property()
65                .in_detail(false),
66        )
67        .field(text("street"))
68        .field(text("number"))
69        .field(text("complement").in_list(false))
70        .field(text("city"))
71        .field(text("state"))
72        .field(text("postal_code").caption("Postal code").in_list(false))
73        .field(reference("country", "Country"))
74        .rule(Rule::required("street"))
75}
76
77/// A person.
78pub fn person() -> EntityInfo {
79    EntityInfo::new("Person")
80        .caption("People")
81        .icon("user")
82        .field(key())
83        .field(
84            text("full_name")
85                .caption("Name")
86                .calculated("Trim(Concat([first_name], ' ', [last_name]))")
87                .default_property()
88                .in_detail(false),
89        )
90        .field(text("first_name").caption("First name").in_list(false))
91        .field(text("last_name").caption("Last name").in_list(false))
92        .field(email())
93        .field(text("phone"))
94        .field(FieldInfo::new("birthday", FieldKind::Date).in_list(false))
95        .field(reference("address", "Address").in_list(false))
96        .rule(Rule::required("first_name"))
97        .rule(Rule::matches("email", EMAIL))
98}
99
100/// A company or any other organization.
101pub fn organization() -> EntityInfo {
102    EntityInfo::new("Organization")
103        .caption("Organizations")
104        .icon("users")
105        .field(key())
106        .field(text("name").default_property())
107        .field(email())
108        .field(text("phone"))
109        .field(text("website").in_list(false))
110        .field(reference("address", "Address").in_list(false))
111        .rule(Rule::required("name"))
112        .rule(Rule::matches("email", EMAIL))
113}
114
115/// Something to do.
116pub fn task() -> EntityInfo {
117    let status = vec![
118        EnumOption::new("not_started", "Not started"),
119        EnumOption::new("in_progress", "In progress"),
120        EnumOption::new("waiting", "Waiting"),
121        EnumOption::new("done", "Done"),
122        EnumOption::new("deferred", "Deferred"),
123    ];
124    EntityInfo::new("Task")
125        .caption("Tasks")
126        .icon("clipboard")
127        .field(key())
128        .field(text("subject").default_property())
129        .field(FieldInfo::new("status", FieldKind::Enum(status)))
130        .field(FieldInfo::new("assigned_to", FieldKind::Reference("Person".into())).caption("Assigned to"))
131        .field(FieldInfo::new("due_date", FieldKind::Date).caption("Due date"))
132        .field(
133            FieldInfo::new("percent_done", FieldKind::Integer)
134                .caption("Done")
135                .display_format("{} %")
136                .default_value(0.0),
137        )
138        .field(FieldInfo::new("description", FieldKind::LongText))
139        .rule(Rule::required("subject"))
140        .rule(Rule::range("percent_done", Some(0.0), Some(100.0)))
141}
142
143/// Something that happens at a time.
144pub fn event() -> EntityInfo {
145    EntityInfo::new("Event")
146        .caption("Events")
147        .icon("calendar")
148        .field(key())
149        .field(text("subject").default_property())
150        .field(FieldInfo::new("starts", FieldKind::DateTime))
151        .field(FieldInfo::new("ends", FieldKind::DateTime))
152        .field(FieldInfo::new("remind_at", FieldKind::DateTime).caption("Remind at").in_list(false))
153        .field(FieldInfo::new("all_day", FieldKind::Boolean).caption("All day"))
154        .field(text("location"))
155        .field(FieldInfo::new("description", FieldKind::LongText))
156        .reminder(ravenrs_app::Reminder::new("remind_at"))
157        .rule(Rule::required("subject"))
158        .rule(Rule::required("starts"))
159        .rule(
160            Rule::on(
161                "ends",
162                RuleKind::Compare {
163                    comparison: Comparison::GreaterOrEqual,
164                    with: Operand::Field("starts".into()),
165                },
166            )
167            .target_criteria("Not IsNull([starts])"),
168        )
169}
170
171/// Something written down.
172pub fn note() -> EntityInfo {
173    EntityInfo::new("Note")
174        .caption("Notes")
175        .icon("file-text")
176        .field(key())
177        .field(text("title").default_property())
178        .field(FieldInfo::new("text", FieldKind::LongText))
179        .rule(Rule::required("title"))
180}
181
182/// A file, with a name of its own.
183pub fn file_record() -> EntityInfo {
184    EntityInfo::new("FileRecord")
185        .caption("Files")
186        .icon("file")
187        .field(key())
188        .field(text("name").default_property())
189        .field(FieldInfo::new("file", FieldKind::File))
190        .rule(Rule::required("file"))
191}
192
193/// Brings these records into an application, each with a place in the navigation if given one.
194/// What they point to in this library comes along by itself, out of the navigation.
195#[derive(Debug, Default)]
196pub struct LibraryModule {
197    entities: Vec<EntityInfo>,
198    navigation: Option<String>,
199}
200
201impl LibraryModule {
202    /// A module with no records yet.
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Puts the records named here in this group of the navigation.
208    pub fn navigation(mut self, group: impl Into<String>) -> Self {
209        self.navigation = Some(group.into());
210        self
211    }
212
213    /// Brings in this record, as given, which may be one of this library with more on it. A second
214    /// by the same name takes the place of the first.
215    pub fn entity(mut self, entity: EntityInfo) -> Self {
216        self.entities.retain(|known| known.name != entity.name);
217        self.entities.push(entity);
218        self
219    }
220
221    /// Brings in [`person`].
222    pub fn person(self) -> Self {
223        self.entity(person())
224    }
225
226    /// Brings in [`organization`].
227    pub fn organization(self) -> Self {
228        self.entity(organization())
229    }
230
231    /// Brings in [`address`].
232    pub fn address(self) -> Self {
233        self.entity(address())
234    }
235
236    /// Brings in [`country`].
237    pub fn country(self) -> Self {
238        self.entity(country())
239    }
240
241    /// Brings in [`task`].
242    pub fn task(self) -> Self {
243        self.entity(task())
244    }
245
246    /// Brings in [`event`].
247    pub fn event(self) -> Self {
248        self.entity(event())
249    }
250
251    /// Brings in [`note`].
252    pub fn note(self) -> Self {
253        self.entity(note())
254    }
255
256    /// Brings in [`file_record`].
257    pub fn file_record(self) -> Self {
258        self.entity(file_record())
259    }
260}
261
262/// A record of this library by its name.
263fn known(name: &str) -> Option<EntityInfo> {
264    match name {
265        "Person" => Some(person()),
266        "Organization" => Some(organization()),
267        "Address" => Some(address()),
268        "Country" => Some(country()),
269        _ => None,
270    }
271}
272
273impl Module for LibraryModule {
274    fn name(&self) -> &str {
275        "Library"
276    }
277
278    fn entities(&self) -> Vec<EntityInfo> {
279        let mut all: Vec<EntityInfo> = self
280            .entities
281            .iter()
282            .map(|entity| match (&self.navigation, entity.navigation.is_none()) {
283                (Some(group), true) => entity.clone().navigation(group.clone()),
284                _ => entity.clone(),
285            })
286            .collect();
287        let mut at = 0;
288        while at < all.len() {
289            let needed: Vec<String> = all[at]
290                .fields
291                .iter()
292                .filter_map(|field| match &field.kind {
293                    FieldKind::Reference(target) => Some(target.clone()),
294                    _ => None,
295                })
296                .collect();
297            for name in needed {
298                if !all.iter().any(|entity| entity.name == name)
299                    && let Some(entity) = known(&name)
300                {
301                    all.push(entity);
302                }
303            }
304            at += 1;
305        }
306        all
307    }
308}