forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4use crate::{errors::expand_error, i18n::Locales};
5
6use super::cache_countries::cached_countries;
7
8#[derive(Debug, Error)]
9pub enum BuildEventError {
10 #[error("error-event-builder-1 Invalid Name")]
11 InvalidName,
12
13 #[error("error-event-builder-2 Invalid Description")]
14 InvalidDescription,
15
16 #[error("error-event-builder-3 Invalid Time Zone")]
17 InvalidTimeZone,
18
19 #[error("error-event-builder-4 Invalid Status")]
20 InvalidStatus,
21
22 #[error("error-event-builder-5 Invalid Mode")]
23 InvalidMode,
24
25 #[error("error-event-builder-6 Invalid Start Date/Time Format")]
26 InvalidStartDateTime,
27
28 #[error("error-event-builder-7 Invalid End Date/Time Format")]
29 InvalidEndDateTime,
30
31 #[error("error-event-builder-8 End Date/Time Must Be After Start Date/Time")]
32 EndBeforeStart,
33
34 #[error("error-event-builder-9 Address Location Country Missing")]
35 LocationCountryRequired,
36
37 #[error("error-event-builder-10 Invalid Address Location Country: {0}")]
38 LocationCountryInvalid(String),
39
40 #[error("error-event-builder-11 Invalid Address Location Locality")]
41 InvalidLocationAddressLocality,
42
43 #[error("error-event-builder-12 Invalid Address Location Region")]
44 InvalidLocationAddressRegion,
45
46 #[error("error-event-builder-13 Invalid Address Location Street")]
47 InvalidLocationAddressStreet,
48
49 #[error("error-event-builder-14 Invalid Address Location Postal Code")]
50 InvalidLocationAddressPostalCode,
51
52 #[error("error-event-builder-15 Invalid Address Location Name")]
53 InvalidLocationAddressName,
54
55 #[error("error-event-builder-16 Invalid Link URL")]
56 InvalidLinkValue,
57
58 #[error("error-event-builder-17 Invalid Link Name")]
59 InvalidLinkName,
60}
61
62#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone)]
63pub enum BuildEventContentState {
64 #[default]
65 Reset,
66 Selecting,
67 Selected,
68}
69
70#[derive(Serialize, Deserialize, Debug, Clone)]
71pub struct BuildStartsForm {
72 pub build_state: Option<BuildEventContentState>,
73
74 pub tz: Option<String>,
75 pub tz_error: Option<String>,
76
77 pub starts_date: Option<String>,
78 pub starts_date_error: Option<String>,
79
80 pub starts_time: Option<String>,
81 pub starts_time_error: Option<String>,
82
83 pub starts_at: Option<String>,
84 pub starts_at_error: Option<String>,
85
86 pub include_ends: Option<bool>,
87
88 pub ends_date: Option<String>,
89 pub ends_date_error: Option<String>,
90
91 pub ends_time: Option<String>,
92 pub ends_time_error: Option<String>,
93
94 pub ends_at: Option<String>,
95 pub ends_at_error: Option<String>,
96
97 pub starts_display: Option<String>,
98 pub ends_display: Option<String>,
99}
100
101#[derive(Serialize, Deserialize, Debug, Clone)]
102pub struct BuildLocationForm {
103 pub build_state: Option<BuildEventContentState>,
104
105 pub location_country: Option<String>,
106 pub location_country_error: Option<String>,
107
108 pub location_street: Option<String>,
109 pub location_street_error: Option<String>,
110
111 pub location_locality: Option<String>,
112 pub location_locality_error: Option<String>,
113
114 pub location_region: Option<String>,
115 pub location_region_error: Option<String>,
116
117 pub location_postal_code: Option<String>,
118 pub location_postal_code_error: Option<String>,
119
120 pub location_name: Option<String>,
121 pub location_name_error: Option<String>,
122}
123
124#[derive(Serialize, Deserialize, Debug, Clone)]
125pub struct BuildLinkForm {
126 pub build_state: Option<BuildEventContentState>,
127
128 pub link_name: Option<String>,
129 pub link_name_error: Option<String>,
130
131 pub link_value: Option<String>,
132 pub link_value_error: Option<String>,
133}
134
135#[derive(Serialize, Deserialize, Debug, Clone)]
136pub struct BuildEventForm {
137 pub build_state: Option<BuildEventContentState>,
138
139 pub name: Option<String>,
140 pub name_error: Option<String>,
141
142 pub description: Option<String>,
143 pub description_error: Option<String>,
144
145 pub status: Option<String>,
146 pub status_error: Option<String>,
147
148 pub starts_at: Option<String>,
149 pub starts_at_error: Option<String>,
150
151 pub ends_at: Option<String>,
152 pub ends_at_error: Option<String>,
153
154 pub mode: Option<String>,
155 pub mode_error: Option<String>,
156
157 pub location_country: Option<String>,
158 pub location_country_error: Option<String>,
159
160 pub location_street: Option<String>,
161 pub location_street_error: Option<String>,
162
163 pub location_locality: Option<String>,
164 pub location_locality_error: Option<String>,
165
166 pub location_region: Option<String>,
167 pub location_region_error: Option<String>,
168
169 pub location_postal_code: Option<String>,
170 pub location_postal_code_error: Option<String>,
171
172 pub location_name: Option<String>,
173 pub location_name_error: Option<String>,
174
175 pub link_name: Option<String>,
176 pub link_name_error: Option<String>,
177
178 pub link_value: Option<String>,
179 pub link_value_error: Option<String>,
180}
181
182impl From<BuildEventForm> for BuildLocationForm {
183 fn from(build_event_form: BuildEventForm) -> Self {
184 BuildLocationForm {
185 build_state: build_event_form.build_state,
186 location_country: None,
187 location_country_error: None,
188 location_name: None,
189 location_name_error: None,
190 location_street: None,
191 location_street_error: None,
192 location_locality: None,
193 location_locality_error: None,
194 location_region: None,
195 location_region_error: None,
196 location_postal_code: None,
197 location_postal_code_error: None,
198 }
199 }
200}
201
202impl From<BuildEventForm> for BuildStartsForm {
203 fn from(build_event_form: BuildEventForm) -> Self {
204 BuildStartsForm {
205 build_state: build_event_form.build_state,
206 tz: None,
207 tz_error: None,
208 starts_date: None,
209 starts_date_error: None,
210 starts_time: None,
211 starts_time_error: None,
212 starts_at: build_event_form.starts_at,
213 starts_at_error: None,
214 include_ends: None,
215 ends_date: None,
216 ends_date_error: None,
217 ends_time: None,
218 ends_time_error: None,
219 ends_at: build_event_form.ends_at,
220 ends_at_error: None,
221 starts_display: None,
222 ends_display: None,
223 }
224 }
225}
226
227impl From<BuildEventForm> for BuildLinkForm {
228 fn from(build_event_form: BuildEventForm) -> Self {
229 BuildLinkForm {
230 build_state: build_event_form.build_state,
231 link_name: None,
232 link_name_error: None,
233 link_value: None,
234 link_value_error: None,
235 }
236 }
237}
238
239impl BuildLocationForm {
240 pub fn validate(
241 &mut self,
242 locales: &Locales,
243 language: &unic_langid::LanguageIdentifier,
244 ) -> bool {
245 if let Some(location_country_value) = self.location_country.as_ref() {
246 let all_countries = match cached_countries() {
247 Ok(value) => value,
248 Err(err) => {
249 let (err_bare, err_partial) = expand_error(err);
250 let error_message = locales.format_error(language, &err_bare, &err_partial);
251 self.location_country_error = Some(error_message);
252 return true;
253 }
254 };
255
256 if !all_countries.contains_key(location_country_value) {
257 let (err_bare, err_partial) = expand_error(
258 BuildEventError::LocationCountryInvalid(location_country_value.clone()),
259 );
260 let error_message = locales.format_error(language, &err_bare, &err_partial);
261 self.location_country_error = Some(error_message);
262 return true;
263 }
264 } else {
265 let (err_bare, err_partial) = expand_error(BuildEventError::LocationCountryRequired);
266 let error_message = locales.format_error(language, &err_bare, &err_partial);
267 self.location_country_error = Some(error_message);
268 return true;
269 }
270
271 let mut found_errors = false;
272
273 if let Some(user_value) = &self.location_locality {
274 let trimmed_user_value = user_value.trim();
275 if trimmed_user_value.is_empty() || trimmed_user_value.len() > 200 {
276 let (err_bare, err_partial) =
277 expand_error(BuildEventError::InvalidLocationAddressLocality);
278 let error_message = locales.format_error(language, &err_bare, &err_partial);
279 self.location_locality_error = Some(error_message);
280 found_errors = true;
281 }
282
283 if trimmed_user_value != user_value {
284 let trimmed_string = trimmed_user_value.to_string();
285 self.location_locality = Some(trimmed_string);
286 found_errors = true;
287 }
288 }
289
290 if let Some(user_value) = &self.location_region {
291 let trimmed_user_value = user_value.trim();
292 if trimmed_user_value.is_empty() || trimmed_user_value.len() > 200 {
293 let (err_bare, err_partial) =
294 expand_error(BuildEventError::InvalidLocationAddressRegion);
295 let error_message = locales.format_error(language, &err_bare, &err_partial);
296 self.location_region_error = Some(error_message);
297 found_errors = true;
298 }
299
300 if trimmed_user_value != user_value {
301 let trimmed_string = trimmed_user_value.to_string();
302 self.location_region = Some(trimmed_string);
303 found_errors = true;
304 }
305 }
306
307 if let Some(user_value) = &self.location_street {
308 let trimmed_user_value = user_value.trim();
309 if trimmed_user_value.is_empty() || trimmed_user_value.len() > 200 {
310 let (err_bare, err_partial) =
311 expand_error(BuildEventError::InvalidLocationAddressStreet);
312 let error_message = locales.format_error(language, &err_bare, &err_partial);
313 self.location_street_error = Some(error_message);
314 found_errors = true;
315 }
316
317 if trimmed_user_value != user_value {
318 let trimmed_string = trimmed_user_value.to_string();
319 self.location_street = Some(trimmed_string);
320 found_errors = true;
321 }
322 }
323
324 if let Some(user_value) = &self.location_postal_code {
325 let trimmed_user_value = user_value.trim();
326 if trimmed_user_value.is_empty() || trimmed_user_value.len() > 200 {
327 let (err_bare, err_partial) =
328 expand_error(BuildEventError::InvalidLocationAddressPostalCode);
329 let error_message = locales.format_error(language, &err_bare, &err_partial);
330 self.location_postal_code_error = Some(error_message);
331 found_errors = true;
332 }
333
334 if trimmed_user_value != user_value {
335 let trimmed_string = trimmed_user_value.to_string();
336 self.location_postal_code = Some(trimmed_string);
337 found_errors = true;
338 }
339 }
340
341 if let Some(user_value) = &self.location_name {
342 let trimmed_user_value = user_value.trim();
343 if trimmed_user_value.is_empty() || trimmed_user_value.len() > 200 {
344 let (err_bare, err_partial) =
345 expand_error(BuildEventError::InvalidLocationAddressName);
346 let error_message = locales.format_error(language, &err_bare, &err_partial);
347 self.location_name_error = Some(error_message);
348 found_errors = true;
349 }
350
351 if trimmed_user_value != user_value {
352 let trimmed_string = trimmed_user_value.to_string();
353 self.location_name = Some(trimmed_string);
354 found_errors = true;
355 }
356 }
357
358 found_errors
359 }
360}
361
362impl BuildLinkForm {
363 pub fn validate(
364 &mut self,
365 locales: &Locales,
366 language: &unic_langid::LanguageIdentifier,
367 ) -> bool {
368 let mut found_errors = false;
369
370 // Validate link URL (required)
371 if let Some(link_value) = &self.link_value {
372 let trimmed_value = link_value.trim();
373
374 // Check if the URL is valid
375 if trimmed_value.is_empty()
376 || trimmed_value.len() > 500
377 || (!trimmed_value.starts_with("http://") && !trimmed_value.starts_with("https://"))
378 {
379 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidLinkValue);
380 let error_message = locales.format_error(language, &err_bare, &err_partial);
381 self.link_value_error = Some(error_message);
382 found_errors = true;
383 }
384
385 // Replace original value with trimmed value if different
386 if trimmed_value != link_value {
387 let trimmed_string = trimmed_value.to_string();
388 self.link_value = Some(trimmed_string);
389 found_errors = true;
390 }
391 } else {
392 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidLinkValue);
393 let error_message = locales.format_error(language, &err_bare, &err_partial);
394 self.link_value_error = Some(error_message);
395 found_errors = true;
396 }
397
398 // Validate link name (optional)
399 if let Some(name_value) = &self.link_name {
400 let trimmed_name = name_value.trim();
401
402 // Only validate if not empty
403 if !trimmed_name.is_empty() && trimmed_name.len() > 200 {
404 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidLinkName);
405 let error_message = locales.format_error(language, &err_bare, &err_partial);
406 self.link_name_error = Some(error_message);
407 found_errors = true;
408 }
409
410 // Replace original value with trimmed value if different
411 if trimmed_name != name_value {
412 let trimmed_string = trimmed_name.to_string();
413 self.link_name = Some(trimmed_string);
414 found_errors = true;
415 }
416 }
417
418 found_errors
419 }
420}
421
422impl BuildStartsForm {
423 pub fn validate(
424 &mut self,
425 locales: &Locales,
426 language: &unic_langid::LanguageIdentifier,
427 ) -> bool {
428 if self.tz.is_none() {
429 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidTimeZone);
430 let error_message = locales.format_error(language, &err_bare, &err_partial);
431 self.tz_error = Some(error_message);
432 return true;
433 }
434
435 let tz = self.tz.as_ref().unwrap().parse();
436
437 if tz.is_err() {
438 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidTimeZone);
439 let error_message = locales.format_error(language, &err_bare, &err_partial);
440 self.tz_error = Some(error_message);
441 return true;
442 }
443
444 let has_starts = self.starts_date.is_some() && self.starts_time.is_some();
445
446 let tz: chrono_tz::Tz = tz.unwrap();
447
448 let mut found_errors = false;
449
450 let starts_at = if has_starts {
451 let date_str = self.starts_date.clone().unwrap_or_default();
452 let time_str = self.starts_time.clone().unwrap_or_default();
453
454 match crate::http::timezones::combine_html_datetime(&date_str, &time_str, tz) {
455 Ok(utc_dt) => {
456 self.starts_at = Some(utc_dt.to_string());
457 self.starts_display = Some(
458 utc_dt
459 .with_timezone(&tz)
460 .format("%A, %B %-d, %Y %r %Z")
461 .to_string(),
462 );
463 Some(utc_dt)
464 }
465 Err(_) => {
466 found_errors = true;
467 let (err_bare, err_partial) =
468 expand_error(BuildEventError::InvalidStartDateTime);
469 let error_message = locales.format_error(language, &err_bare, &err_partial);
470 self.starts_at_error = Some(error_message);
471 None
472 }
473 }
474 } else {
475 None
476 };
477
478 if self.include_ends.is_some_and(|v| v) {
479 let has_ends = self.ends_date.is_some() && self.ends_time.is_some();
480 if has_starts && !has_ends {
481 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidEndDateTime);
482 let error_message = locales.format_error(language, &err_bare, &err_partial);
483 self.ends_date_error = Some(error_message);
484 found_errors = true;
485 }
486 let ends_at = if has_starts && has_ends {
487 let date_str = self.ends_date.clone().unwrap_or_default();
488 let time_str = self.ends_time.clone().unwrap_or_default();
489
490 match crate::http::timezones::combine_html_datetime(&date_str, &time_str, tz) {
491 Ok(utc_dt) => {
492 self.ends_at = Some(utc_dt.to_string());
493 self.ends_display = Some(
494 utc_dt
495 .with_timezone(&tz)
496 .format("%A, %B %-d, %Y %r %Z")
497 .to_string(),
498 );
499 Some(utc_dt)
500 }
501 Err(_) => {
502 found_errors = true;
503 let (err_bare, err_partial) =
504 expand_error(BuildEventError::InvalidEndDateTime);
505 let error_message = locales.format_error(language, &err_bare, &err_partial);
506 self.ends_at_error = Some(error_message);
507 None
508 }
509 }
510 } else {
511 None
512 };
513
514 if starts_at.is_some_and(|start| ends_at.is_some_and(|end| start > end)) {
515 let (err_bare, err_partial) = expand_error(BuildEventError::EndBeforeStart);
516 let error_message = locales.format_error(language, &err_bare, &err_partial);
517 self.ends_at_error = Some(error_message);
518 found_errors = true;
519 }
520 }
521
522 found_errors
523 }
524}
525
526impl BuildEventForm {
527 pub fn validate(
528 &mut self,
529 locales: &Locales,
530 language: &unic_langid::LanguageIdentifier,
531 ) -> bool {
532 let mut found_errors = false;
533
534 // Validate name field
535 if let Some(name_value) = &self.name {
536 // Properly handle whitespace by trimming
537 let trimmed_name = name_value.trim();
538
539 // Check length requirements
540 if trimmed_name.len() < 10 || trimmed_name.len() > 500 {
541 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidName);
542 let error_message = locales.format_error(language, &err_bare, &err_partial);
543 self.name_error = Some(error_message);
544 found_errors = true;
545 }
546
547 // Replace original value with trimmed value if different
548 if trimmed_name != name_value {
549 // Create a new string to avoid borrowing issues
550 let trimmed_string = trimmed_name.to_string();
551 // Assign the new value to self.name
552 self.name = Some(trimmed_string);
553 }
554 } else {
555 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidName);
556 let error_message = locales.format_error(language, &err_bare, &err_partial);
557 self.name_error = Some(error_message);
558 found_errors = true;
559 }
560
561 // Validate description field
562 if let Some(desc_value) = &self.description {
563 // Properly handle whitespace by trimming
564 let trimmed_desc = desc_value.trim();
565
566 // Check character limits
567 if trimmed_desc.len() < 10 || trimmed_desc.len() > 3000 {
568 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidDescription);
569 let error_message = locales.format_error(language, &err_bare, &err_partial);
570 self.description_error = Some(error_message);
571 found_errors = true;
572 }
573
574 // Replace original value with trimmed value if different
575 if trimmed_desc != desc_value {
576 // Create a new string to avoid borrowing issues
577 let trimmed_string = trimmed_desc.to_string();
578 // Assign the new value to self.description
579 self.description = Some(trimmed_string);
580 }
581 } else {
582 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidDescription);
583 let error_message = locales.format_error(language, &err_bare, &err_partial);
584 self.description_error = Some(error_message);
585 found_errors = true;
586 }
587
588 // Validate status field
589 if let Some(status) = &self.status {
590 let valid_statuses = [
591 "planned",
592 "scheduled",
593 "cancelled",
594 "postponed",
595 "rescheduled",
596 ];
597 if !valid_statuses.contains(&status.as_str()) {
598 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidStatus);
599 let error_message = locales.format_error(language, &err_bare, &err_partial);
600 self.status_error = Some(error_message);
601 found_errors = true;
602 }
603 } else {
604 // Default to planned if not provided
605 self.status = Some("planned".to_string());
606 }
607
608 // Validate mode field
609 if let Some(mode) = &self.mode {
610 let valid_modes = ["inperson", "virtual", "hybrid"];
611 if !valid_modes.contains(&mode.as_str()) {
612 let (err_bare, err_partial) = expand_error(BuildEventError::InvalidMode);
613 let error_message = locales.format_error(language, &err_bare, &err_partial);
614 self.mode_error = Some(error_message);
615 found_errors = true;
616 }
617 } else {
618 // Default to inperson if not provided
619 self.mode = Some("inperson".to_string());
620 }
621
622 found_errors
623 }
624}