i18n+filtering fork - fluent-templates v2
at main 624 lines 23 kB view raw
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}