ravenrs_app_library/
lib.rs1use ravenrs_app::{
17 Comparison, EntityInfo, EnumOption, FieldInfo, FieldKind, Module, Operand, Rule, RuleKind,
18};
19
20const EMAIL: &str = r"^[^@\s]+@[^@\s]+\.[^@\s]+$";
22
23pub 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
42pub 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
54pub 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
77pub 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
100pub 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
115pub 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
143pub 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
171pub 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
182pub 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#[derive(Debug, Default)]
196pub struct LibraryModule {
197 entities: Vec<EntityInfo>,
198 navigation: Option<String>,
199}
200
201impl LibraryModule {
202 pub fn new() -> Self {
204 Self::default()
205 }
206
207 pub fn navigation(mut self, group: impl Into<String>) -> Self {
209 self.navigation = Some(group.into());
210 self
211 }
212
213 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 pub fn person(self) -> Self {
223 self.entity(person())
224 }
225
226 pub fn organization(self) -> Self {
228 self.entity(organization())
229 }
230
231 pub fn address(self) -> Self {
233 self.entity(address())
234 }
235
236 pub fn country(self) -> Self {
238 self.entity(country())
239 }
240
241 pub fn task(self) -> Self {
243 self.entity(task())
244 }
245
246 pub fn event(self) -> Self {
248 self.entity(event())
249 }
250
251 pub fn note(self) -> Self {
253 self.entity(note())
254 }
255
256 pub fn file_record(self) -> Self {
258 self.entity(file_record())
259 }
260}
261
262fn 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}