use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use super::{
EventAttendanceMode, EventStatus, EventType, Identifier, Location, Offer, Party, Reference,
};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct Event {
pub id: Uuid,
#[serde(default)]
pub identifiers: Vec<Identifier>,
pub active: bool,
pub name: String,
#[serde(default)]
pub alternate_names: Vec<String>,
pub description: Option<String>,
pub disambiguating_description: Option<String>,
pub url: Option<String>,
#[serde(default)]
pub image: Vec<String>,
#[serde(default)]
pub same_as: Vec<String>,
#[serde(default)]
pub keywords: Vec<String>,
pub start_date: DateTime<Utc>,
pub end_date: Option<DateTime<Utc>>,
pub door_time: Option<DateTime<Utc>>,
pub duration: Option<String>,
pub previous_start_date: Option<DateTime<Utc>>,
pub time_zone: Option<String>,
#[serde(default)]
pub all_day: bool,
pub event_status: EventStatus,
pub event_attendance_mode: EventAttendanceMode,
pub event_type: EventType,
pub typical_age_range: Option<String>,
#[serde(default)]
pub in_language: Vec<String>,
pub is_accessible_for_free: Option<bool>,
pub maximum_attendee_capacity: Option<u32>,
pub maximum_physical_attendee_capacity: Option<u32>,
pub maximum_virtual_attendee_capacity: Option<u32>,
pub remaining_attendee_capacity: Option<u32>,
#[serde(default)]
pub location: Vec<Location>,
#[serde(default)]
pub organizers: Vec<Party>,
#[serde(default)]
pub performers: Vec<Party>,
#[serde(default)]
pub attendees: Vec<Party>,
#[serde(default)]
pub sponsors: Vec<Party>,
#[serde(default)]
pub funders: Vec<Party>,
#[serde(default)]
pub contributors: Vec<Party>,
#[serde(default)]
pub about: Vec<Reference>,
#[serde(default)]
pub works: Vec<Reference>,
pub super_event: Option<Uuid>,
#[serde(default)]
pub sub_events: Vec<Uuid>,
#[serde(default)]
pub offers: Vec<Offer>,
#[serde(default)]
pub links: Vec<EventLink>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct EventLink {
pub other_event_id: Uuid,
pub link_type: LinkType,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LinkType {
ReplacedBy,
Replaces,
Refer,
Seealso,
}
impl Event {
pub fn new(name: impl Into<String>, start_date: DateTime<Utc>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
identifiers: Vec::new(),
active: true,
name: name.into(),
alternate_names: Vec::new(),
description: None,
disambiguating_description: None,
url: None,
image: Vec::new(),
same_as: Vec::new(),
keywords: Vec::new(),
start_date,
end_date: None,
door_time: None,
duration: None,
previous_start_date: None,
time_zone: None,
all_day: false,
event_status: EventStatus::default(),
event_attendance_mode: EventAttendanceMode::default(),
event_type: EventType::default(),
typical_age_range: None,
in_language: Vec::new(),
is_accessible_for_free: None,
maximum_attendee_capacity: None,
maximum_physical_attendee_capacity: None,
maximum_virtual_attendee_capacity: None,
remaining_attendee_capacity: None,
location: Vec::new(),
organizers: Vec::new(),
performers: Vec::new(),
attendees: Vec::new(),
sponsors: Vec::new(),
funders: Vec::new(),
contributors: Vec::new(),
about: Vec::new(),
works: Vec::new(),
super_event: None,
sub_events: Vec::new(),
offers: Vec::new(),
links: Vec::new(),
created_at: now,
updated_at: now,
}
}
pub fn identifier_value(&self, kind: super::IdentifierType) -> Option<&str> {
self.identifiers
.iter()
.find(|id| id.identifier_type == kind)
.map(|id| id.value.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Address, Location, Party, PartyKind, Place, VirtualLocation};
use chrono::TimeZone;
fn jan_2026() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 15, 9, 0, 0).unwrap()
}
#[test]
fn new_event_defaults() {
let event = Event::new("Annual Conference", jan_2026());
assert!(event.active);
assert_eq!(event.name, "Annual Conference");
assert_eq!(event.event_status, EventStatus::Scheduled);
assert_eq!(event.event_attendance_mode, EventAttendanceMode::Offline);
assert_eq!(event.event_type, EventType::Generic);
assert!(event.identifiers.is_empty());
assert!(event.location.is_empty());
assert!(event.organizers.is_empty());
assert!(event.end_date.is_none());
}
#[test]
fn roundtrip_serde() {
let mut event = Event::new("Concert", jan_2026());
event.event_type = EventType::Music;
event.event_attendance_mode = EventAttendanceMode::Mixed;
event.location.push(Location::Place(Place {
id: None,
name: "Greek Theatre".into(),
address: Some(Address {
use_type: None,
line1: Some("2700 Hearst Ave".into()),
line2: None,
city: Some("Berkeley".into()),
state: Some("CA".into()),
postal_code: Some("94720".into()),
country: Some("US".into()),
}),
latitude: Some(37.873),
longitude: Some(-122.254),
url: None,
}));
event.location.push(Location::Virtual(VirtualLocation {
name: Some("Livestream".into()),
url: "https://example.test/stream".into(),
}));
event.organizers.push(Party {
kind: PartyKind::Organization,
id: None,
name: "Cal Performances".into(),
email: None,
url: None,
});
event.keywords.push("music".into());
event.in_language.push("en".into());
let json = serde_json::to_string(&event).unwrap();
let back: Event = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "Concert");
assert_eq!(back.event_type, EventType::Music);
assert_eq!(back.location.len(), 2);
assert_eq!(back.organizers.len(), 1);
assert_eq!(back.keywords, vec!["music".to_string()]);
}
#[test]
fn location_variants_roundtrip() {
let cases = vec![
Location::Text {
value: "TBA".into(),
},
Location::Virtual(VirtualLocation {
name: None,
url: "https://example.test/zoom".into(),
}),
Location::PostalAddress(Address {
use_type: None,
line1: Some("1 Infinite Loop".into()),
line2: None,
city: Some("Cupertino".into()),
state: Some("CA".into()),
postal_code: Some("95014".into()),
country: Some("US".into()),
}),
];
for loc in cases {
let json = serde_json::to_string(&loc).unwrap();
let back: Location = serde_json::from_str(&json).unwrap();
assert_eq!(back, loc);
}
}
}