use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Address {
pub line1: Option<String>,
pub line2: Option<String>,
pub city: Option<String>,
pub county: Option<String>,
pub postcode: Option<String>,
pub country: Option<String>,
}
impl Address {
pub fn new() -> Self {
Self {
line1: None,
line2: None,
city: None,
county: None,
postcode: None,
country: None,
}
}
pub fn with_line1(mut self, value: impl Into<String>) -> Self {
self.line1 = Some(value.into());
self
}
pub fn with_line2(mut self, value: impl Into<String>) -> Self {
self.line2 = Some(value.into());
self
}
pub fn with_city(mut self, value: impl Into<String>) -> Self {
self.city = Some(value.into());
self
}
pub fn with_county(mut self, value: impl Into<String>) -> Self {
self.county = Some(value.into());
self
}
pub fn with_postcode(mut self, value: impl Into<String>) -> Self {
self.postcode = Some(value.into());
self
}
pub fn with_country(mut self, value: impl Into<String>) -> Self {
self.country = Some(value.into());
self
}
}
impl Default for Address {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Location {
pub venue_name: Option<String>,
pub address: Option<Address>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub virtual_url: Option<String>,
}
impl Location {
pub fn new() -> Self {
Self {
venue_name: None,
address: None,
latitude: None,
longitude: None,
virtual_url: None,
}
}
pub fn with_venue_name(mut self, value: impl Into<String>) -> Self {
self.venue_name = Some(value.into());
self
}
pub fn with_address(mut self, value: Address) -> Self {
self.address = Some(value);
self
}
pub fn with_latitude(mut self, value: f64) -> Self {
self.latitude = Some(value);
self
}
pub fn with_longitude(mut self, value: f64) -> Self {
self.longitude = Some(value);
self
}
pub fn with_virtual_url(mut self, value: impl Into<String>) -> Self {
self.virtual_url = Some(value.into());
self
}
}
impl Default for Location {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EventCategory {
BusinessEvent,
ChildrensEvent,
ComedyEvent,
ConferenceEvent,
CourseInstance,
DanceEvent,
DeliveryEvent,
EducationEvent,
EventSeries,
ExhibitionEvent,
Festival,
FoodEvent,
Hackathon,
LiteraryEvent,
MusicEvent,
PerformingArtsEvent,
PublicationEvent,
SaleEvent,
ScreeningEvent,
SocialEvent,
SportsEvent,
TheaterEvent,
VisualArtsEvent,
Other(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EventStatus {
EventScheduled,
EventCancelled,
EventPostponed,
EventRescheduled,
EventMovedOnline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EventAttendanceMode {
OfflineEventAttendanceMode,
OnlineEventAttendanceMode,
MixedEventAttendanceMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EventIdScheme {
Wikidata,
Eventbrite,
Meetup,
Ticketmaster,
Songkick,
Bandsintown,
Facebook,
Luma,
GoogleCalendar,
ICalendarUid,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EventId {
pub scheme: EventIdScheme,
pub value: String,
}
impl EventId {
pub fn new(scheme: EventIdScheme, value: impl Into<String>) -> Option<Self> {
let trimmed = value.into().trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(Self {
scheme,
value: trimmed,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Event {
pub name: Option<String>,
pub alternate_names: Vec<String>,
pub description: Option<String>,
pub url: Option<String>,
pub event_ids: Vec<EventId>,
pub local_id: Option<String>,
pub category: Option<EventCategory>,
pub keywords: Vec<String>,
pub in_language: Option<String>,
pub typical_age_range: Option<String>,
pub start_date: Option<String>,
pub end_date: Option<String>,
pub door_time: Option<String>,
pub previous_start_date: Option<String>,
pub event_status: Option<EventStatus>,
pub event_attendance_mode: Option<EventAttendanceMode>,
pub location: Option<Location>,
pub country_code_as_iso_3166_1_alpha_2: Option<String>,
pub organizer: Option<String>,
pub performers: Vec<String>,
pub maximum_attendee_capacity: Option<u32>,
pub maximum_physical_attendee_capacity: Option<u32>,
pub maximum_virtual_attendee_capacity: Option<u32>,
pub is_accessible_for_free: Option<bool>,
pub super_event_id: Option<String>,
}
impl Event {
pub fn builder() -> EventBuilder {
EventBuilder::default()
}
pub fn validate(&self) -> crate::Result<()> {
if self.name.is_none() {
return Err(crate::MatchingError::MissingField(
"name is required".to_string(),
));
}
Ok(())
}
}
#[derive(Default)]
pub struct EventBuilder {
name: Option<String>,
alternate_names: Vec<String>,
description: Option<String>,
url: Option<String>,
event_ids: Vec<EventId>,
local_id: Option<String>,
category: Option<EventCategory>,
keywords: Vec<String>,
in_language: Option<String>,
typical_age_range: Option<String>,
start_date: Option<String>,
end_date: Option<String>,
door_time: Option<String>,
previous_start_date: Option<String>,
event_status: Option<EventStatus>,
event_attendance_mode: Option<EventAttendanceMode>,
location: Option<Location>,
country_code_as_iso_3166_1_alpha_2: Option<String>,
organizer: Option<String>,
performers: Vec<String>,
maximum_attendee_capacity: Option<u32>,
maximum_physical_attendee_capacity: Option<u32>,
maximum_virtual_attendee_capacity: Option<u32>,
is_accessible_for_free: Option<bool>,
super_event_id: Option<String>,
}
impl EventBuilder {
pub fn name<S: Into<String>>(mut self, value: S) -> Self {
self.name = Some(value.into());
self
}
pub fn alternate_names(mut self, value: Vec<String>) -> Self {
self.alternate_names = value;
self
}
pub fn add_alternate_name<S: Into<String>>(mut self, value: S) -> Self {
self.alternate_names.push(value.into());
self
}
pub fn description<S: Into<String>>(mut self, value: S) -> Self {
self.description = Some(value.into());
self
}
pub fn url<S: Into<String>>(mut self, value: S) -> Self {
self.url = Some(value.into());
self
}
pub fn event_ids(mut self, value: Vec<EventId>) -> Self {
self.event_ids = value;
self
}
pub fn add_event_id(mut self, value: EventId) -> Self {
self.event_ids.push(value);
self
}
pub fn local_id<S: Into<String>>(mut self, value: S) -> Self {
self.local_id = Some(value.into());
self
}
pub fn category(mut self, value: EventCategory) -> Self {
self.category = Some(value);
self
}
pub fn keywords(mut self, value: Vec<String>) -> Self {
self.keywords = value;
self
}
pub fn add_keyword<S: Into<String>>(mut self, value: S) -> Self {
self.keywords.push(value.into());
self
}
pub fn in_language<S: Into<String>>(mut self, value: S) -> Self {
self.in_language = Some(value.into());
self
}
pub fn typical_age_range<S: Into<String>>(mut self, value: S) -> Self {
self.typical_age_range = Some(value.into());
self
}
pub fn start_date<S: Into<String>>(mut self, value: S) -> Self {
self.start_date = Some(value.into());
self
}
pub fn end_date<S: Into<String>>(mut self, value: S) -> Self {
self.end_date = Some(value.into());
self
}
pub fn door_time<S: Into<String>>(mut self, value: S) -> Self {
self.door_time = Some(value.into());
self
}
pub fn previous_start_date<S: Into<String>>(mut self, value: S) -> Self {
self.previous_start_date = Some(value.into());
self
}
pub fn event_status(mut self, value: EventStatus) -> Self {
self.event_status = Some(value);
self
}
pub fn event_attendance_mode(mut self, value: EventAttendanceMode) -> Self {
self.event_attendance_mode = Some(value);
self
}
pub fn location(mut self, value: Location) -> Self {
self.location = Some(value);
self
}
pub fn country_code_as_iso_3166_1_alpha_2<S: Into<String>>(mut self, value: S) -> Self {
self.country_code_as_iso_3166_1_alpha_2 = Some(value.into());
self
}
pub fn organizer<S: Into<String>>(mut self, value: S) -> Self {
self.organizer = Some(value.into());
self
}
pub fn performers(mut self, value: Vec<String>) -> Self {
self.performers = value;
self
}
pub fn add_performer<S: Into<String>>(mut self, value: S) -> Self {
self.performers.push(value.into());
self
}
pub fn maximum_attendee_capacity(mut self, value: u32) -> Self {
self.maximum_attendee_capacity = Some(value);
self
}
pub fn maximum_physical_attendee_capacity(mut self, value: u32) -> Self {
self.maximum_physical_attendee_capacity = Some(value);
self
}
pub fn maximum_virtual_attendee_capacity(mut self, value: u32) -> Self {
self.maximum_virtual_attendee_capacity = Some(value);
self
}
pub fn is_accessible_for_free(mut self, value: bool) -> Self {
self.is_accessible_for_free = Some(value);
self
}
pub fn super_event_id<S: Into<String>>(mut self, value: S) -> Self {
self.super_event_id = Some(value.into());
self
}
pub fn build(self) -> Event {
Event {
name: self.name,
alternate_names: self.alternate_names,
description: self.description,
url: self.url,
event_ids: self.event_ids,
local_id: self.local_id,
category: self.category,
keywords: self.keywords,
in_language: self.in_language,
typical_age_range: self.typical_age_range,
start_date: self.start_date,
end_date: self.end_date,
door_time: self.door_time,
previous_start_date: self.previous_start_date,
event_status: self.event_status,
event_attendance_mode: self.event_attendance_mode,
location: self.location,
country_code_as_iso_3166_1_alpha_2: self.country_code_as_iso_3166_1_alpha_2,
organizer: self.organizer,
performers: self.performers,
maximum_attendee_capacity: self.maximum_attendee_capacity,
maximum_physical_attendee_capacity: self.maximum_physical_attendee_capacity,
maximum_virtual_attendee_capacity: self.maximum_virtual_attendee_capacity,
is_accessible_for_free: self.is_accessible_for_free,
super_event_id: self.super_event_id,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn address_new_is_all_none() {
let a = Address::new();
assert!(a.line1.is_none());
assert!(a.line2.is_none());
assert!(a.city.is_none());
assert!(a.county.is_none());
assert!(a.postcode.is_none());
assert!(a.country.is_none());
}
#[test]
fn address_default_matches_new() {
assert_eq!(Address::default(), Address::new());
}
#[test]
fn address_fluent_builders_chain() {
let a = Address::new()
.with_line1("Worthy Farm")
.with_city("Pilton")
.with_postcode("BA4 4BY")
.with_country("United Kingdom");
assert_eq!(a.line1.as_deref(), Some("Worthy Farm"));
assert_eq!(a.city.as_deref(), Some("Pilton"));
assert_eq!(a.postcode.as_deref(), Some("BA4 4BY"));
assert_eq!(a.country.as_deref(), Some("United Kingdom"));
assert!(a.line2.is_none());
assert!(a.county.is_none());
}
#[test]
fn address_round_trips_through_serde() {
let mut a = Address::new();
a.line1 = Some("Worthy Farm".into());
a.postcode = Some("BA4 4BY".into());
let json = serde_json::to_string(&a).expect("serialise");
let back: Address = serde_json::from_str(&json).expect("deserialise");
assert_eq!(a, back);
}
#[test]
fn location_new_is_all_none() {
let l = Location::new();
assert!(l.venue_name.is_none());
assert!(l.address.is_none());
assert!(l.latitude.is_none());
assert!(l.longitude.is_none());
assert!(l.virtual_url.is_none());
}
#[test]
fn location_default_matches_new() {
assert_eq!(Location::default(), Location::new());
}
#[test]
fn location_fluent_builders_chain() {
let l = Location::new()
.with_venue_name("Worthy Farm")
.with_latitude(51.150_3)
.with_longitude(-2.586_2);
assert_eq!(l.venue_name.as_deref(), Some("Worthy Farm"));
assert_eq!(l.latitude, Some(51.150_3));
assert_eq!(l.longitude, Some(-2.586_2));
}
#[test]
fn event_builder_starts_empty() {
let e = Event::builder().build();
assert!(e.name.is_none());
assert!(e.alternate_names.is_empty());
assert!(e.description.is_none());
assert!(e.url.is_none());
assert!(e.event_ids.is_empty());
assert!(e.local_id.is_none());
assert!(e.category.is_none());
assert!(e.keywords.is_empty());
assert!(e.in_language.is_none());
assert!(e.start_date.is_none());
assert!(e.end_date.is_none());
assert!(e.location.is_none());
assert!(e.organizer.is_none());
assert!(e.performers.is_empty());
assert!(e.maximum_attendee_capacity.is_none());
assert!(e.is_accessible_for_free.is_none());
assert!(e.super_event_id.is_none());
}
#[test]
fn event_builder_accepts_all_fields() {
let e = Event::builder()
.name("RustConf 2024")
.add_alternate_name("RustConf '24")
.description("Annual Rust conference")
.url("https://rustconf.com/2024")
.start_date("2024-09-10T09:00:00Z")
.end_date("2024-09-13T17:00:00Z")
.door_time("2024-09-10T08:30:00Z")
.organizer("Rust Foundation")
.add_performer("Niko Matsakis")
.category(EventCategory::ConferenceEvent)
.event_status(EventStatus::EventScheduled)
.event_attendance_mode(EventAttendanceMode::MixedEventAttendanceMode)
.country_code_as_iso_3166_1_alpha_2("US")
.maximum_attendee_capacity(1000)
.is_accessible_for_free(false)
.in_language("en")
.typical_age_range("18-99")
.add_keyword("rust")
.build();
assert_eq!(e.name.as_deref(), Some("RustConf 2024"));
assert_eq!(e.alternate_names.len(), 1);
assert_eq!(e.start_date.as_deref(), Some("2024-09-10T09:00:00Z"));
assert_eq!(e.organizer.as_deref(), Some("Rust Foundation"));
assert_eq!(e.category, Some(EventCategory::ConferenceEvent));
assert_eq!(e.event_status, Some(EventStatus::EventScheduled));
assert_eq!(e.maximum_attendee_capacity, Some(1000));
assert_eq!(e.is_accessible_for_free, Some(false));
}
#[test]
fn event_builder_accepts_str_and_string() {
let e = Event::builder()
.name("RustConf")
.add_alternate_name(String::from("RustConf '24"))
.build();
assert_eq!(e.name.as_deref(), Some("RustConf"));
assert_eq!(e.alternate_names, vec!["RustConf '24".to_string()]);
}
#[test]
fn event_validate_requires_a_name() {
assert!(Event::builder().name("RustConf").build().validate().is_ok());
let err = Event::builder()
.build()
.validate()
.expect_err("should be missing");
assert!(matches!(err, crate::MatchingError::MissingField(_)));
}
#[test]
fn event_round_trips_through_serde() {
let e = Event::builder()
.name("Glastonbury Festival 2024")
.add_alternate_name("Glasto 2024")
.start_date("2024-06-26T09:00:00Z")
.end_date("2024-06-30T23:59:00Z")
.category(EventCategory::Festival)
.add_event_id(EventId::new(EventIdScheme::Wikidata, "Q15290").unwrap())
.country_code_as_iso_3166_1_alpha_2("GB")
.build();
let json = serde_json::to_string(&e).expect("serialise");
let back: Event = serde_json::from_str(&json).expect("deserialise");
assert_eq!(e, back);
}
#[test]
fn alternate_names_setter_replaces_vec() {
let e = Event::builder()
.alternate_names(vec!["X".into(), "Y".into()])
.build();
assert_eq!(e.alternate_names, vec!["X".to_string(), "Y".to_string()]);
}
#[test]
fn event_id_trims_value() {
let id = EventId::new(EventIdScheme::Eventbrite, " abc ").unwrap();
assert_eq!(id.value, "abc");
}
#[test]
fn event_id_rejects_empty() {
assert!(EventId::new(EventIdScheme::Eventbrite, "").is_none());
assert!(EventId::new(EventIdScheme::Eventbrite, " ").is_none());
}
#[test]
fn event_id_equality_is_scheme_scoped() {
let g = EventId::new(EventIdScheme::Eventbrite, "X").unwrap();
let o = EventId::new(EventIdScheme::Meetup, "X").unwrap();
assert_ne!(g, o);
}
#[test]
fn event_id_scheme_other_compares_by_string() {
let a = EventId::new(EventIdScheme::Other("foo".into()), "1").unwrap();
let b = EventId::new(EventIdScheme::Other("bar".into()), "1").unwrap();
let c = EventId::new(EventIdScheme::Other("foo".into()), "1").unwrap();
assert_ne!(a, b);
assert_eq!(a, c);
}
#[test]
fn category_other_compares_by_string() {
assert_ne!(
EventCategory::Other("foo".into()),
EventCategory::Other("bar".into())
);
assert_eq!(
EventCategory::Other("foo".into()),
EventCategory::Other("foo".into())
);
}
#[test]
fn event_status_roundtrips_through_serde() {
let s = EventStatus::EventCancelled;
let json = serde_json::to_string(&s).expect("serialise");
let back: EventStatus = serde_json::from_str(&json).expect("deserialise");
assert_eq!(s, back);
}
#[test]
fn attendance_mode_roundtrips_through_serde() {
let m = EventAttendanceMode::MixedEventAttendanceMode;
let json = serde_json::to_string(&m).expect("serialise");
let back: EventAttendanceMode = serde_json::from_str(&json).expect("deserialise");
assert_eq!(m, back);
}
}