use event_matcher::{
Address as MAddress, Event as MEvent, EventAttendanceMode as MMode, EventCategory as MCategory,
EventId as MEventId, EventIdScheme as MScheme, EventStatus as MStatus, Location as MLocation,
};
use crate::models::{
identifier::{Identifier, IdentifierType},
Address as SAddress, Event, EventAttendanceMode, EventStatus, EventType, Location, Party,
};
pub fn to_matcher_event(e: &Event) -> MEvent {
let mut b = MEvent::builder().name(e.name.trim());
let alts: Vec<String> = e
.alternate_names
.iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !alts.is_empty() {
b = b.alternate_names(alts);
}
if let Some(d) = e.description.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
b = b.description(d);
}
if let Some(u) = e.url.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
b = b.url(u);
}
let ids: Vec<MEventId> = e
.identifiers
.iter()
.filter_map(identifier_to_event_id)
.collect();
if !ids.is_empty() {
b = b.event_ids(ids);
}
let keywords: Vec<String> = e
.keywords
.iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !keywords.is_empty() {
b = b.keywords(keywords);
}
if let Some(lang) = e.in_language.first() {
let lang = lang.trim();
if !lang.is_empty() {
b = b.in_language(lang);
}
}
if let Some(age) = e.typical_age_range.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
b = b.typical_age_range(age);
}
b = b.start_date(e.start_date.to_rfc3339());
if let Some(t) = e.end_date {
b = b.end_date(t.to_rfc3339());
}
if let Some(t) = e.door_time {
b = b.door_time(t.to_rfc3339());
}
if let Some(t) = e.previous_start_date {
b = b.previous_start_date(t.to_rfc3339());
}
b = b.event_status(map_status(e.event_status));
b = b.event_attendance_mode(map_attendance_mode(e.event_attendance_mode));
b = b.category(map_event_type(&e.event_type));
if let Some(loc) = e.location.iter().find_map(map_location) {
b = b.location(loc);
}
if let Some(o) = first_party_name(&e.organizers) {
b = b.organizer(o);
}
let perfs: Vec<String> = e
.performers
.iter()
.map(|p| p.name.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !perfs.is_empty() {
b = b.performers(perfs);
}
if let Some(c) = e.maximum_attendee_capacity {
b = b.maximum_attendee_capacity(c);
}
if let Some(c) = e.maximum_physical_attendee_capacity {
b = b.maximum_physical_attendee_capacity(c);
}
if let Some(c) = e.maximum_virtual_attendee_capacity {
b = b.maximum_virtual_attendee_capacity(c);
}
if let Some(v) = e.is_accessible_for_free {
b = b.is_accessible_for_free(v);
}
if let Some(parent) = e.super_event {
b = b.super_event_id(parent.to_string());
}
b.build()
}
fn first_party_name(parties: &[Party]) -> Option<String> {
parties
.iter()
.map(|p| p.name.trim().to_string())
.find(|s| !s.is_empty())
}
fn map_status(s: EventStatus) -> MStatus {
match s {
EventStatus::Scheduled => MStatus::EventScheduled,
EventStatus::Cancelled => MStatus::EventCancelled,
EventStatus::Postponed => MStatus::EventPostponed,
EventStatus::Rescheduled => MStatus::EventRescheduled,
EventStatus::MovedOnline => MStatus::EventMovedOnline,
EventStatus::Completed => MStatus::EventScheduled,
}
}
fn map_attendance_mode(m: EventAttendanceMode) -> MMode {
match m {
EventAttendanceMode::Offline => MMode::OfflineEventAttendanceMode,
EventAttendanceMode::Online => MMode::OnlineEventAttendanceMode,
EventAttendanceMode::Mixed => MMode::MixedEventAttendanceMode,
}
}
pub fn map_event_type(t: &EventType) -> MCategory {
match t {
EventType::Business => MCategory::BusinessEvent,
EventType::Childrens => MCategory::ChildrensEvent,
EventType::Comedy => MCategory::ComedyEvent,
EventType::Conference => MCategory::ConferenceEvent,
EventType::Course => MCategory::CourseInstance,
EventType::Dance => MCategory::DanceEvent,
EventType::Delivery => MCategory::DeliveryEvent,
EventType::Education => MCategory::EducationEvent,
EventType::Exhibition => MCategory::ExhibitionEvent,
EventType::Festival => MCategory::Festival,
EventType::Food => MCategory::FoodEvent,
EventType::Hackathon => MCategory::Hackathon,
EventType::Literary => MCategory::LiteraryEvent,
EventType::Music => MCategory::MusicEvent,
EventType::PerformingArts => MCategory::PerformingArtsEvent,
EventType::Publication => MCategory::PublicationEvent,
EventType::Sale => MCategory::SaleEvent,
EventType::Screening => MCategory::ScreeningEvent,
EventType::Series => MCategory::EventSeries,
EventType::Social => MCategory::SocialEvent,
EventType::Sports => MCategory::SportsEvent,
EventType::Theater => MCategory::TheaterEvent,
EventType::VisualArts => MCategory::VisualArtsEvent,
EventType::Generic => MCategory::Other("Generic".into()),
EventType::Appointment => MCategory::Other("Appointment".into()),
EventType::Encounter => MCategory::Other("Encounter".into()),
EventType::Shift => MCategory::Other("Shift".into()),
EventType::Incident => MCategory::Other("Incident".into()),
EventType::Session => MCategory::Other("Session".into()),
}
}
fn map_location(l: &Location) -> Option<MLocation> {
let mut m = MLocation::new();
let mut any = false;
match l {
Location::Place(p) => {
if !p.name.trim().is_empty() {
m = m.with_venue_name(p.name.trim());
any = true;
}
if let Some(a) = p.address.as_ref().and_then(map_service_address) {
m = m.with_address(a);
any = true;
}
if let Some(lat) = p.latitude {
m = m.with_latitude(lat);
any = true;
}
if let Some(lon) = p.longitude {
m = m.with_longitude(lon);
any = true;
}
}
Location::PostalAddress(a) => {
if let Some(addr) = map_service_address(a) {
m = m.with_address(addr);
any = true;
}
}
Location::Virtual(v) => {
let url = v.url.trim();
if !url.is_empty() {
m = m.with_virtual_url(url);
any = true;
}
if let Some(n) = v.name.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
m = m.with_venue_name(n);
any = true;
}
}
Location::Text { value } => {
let v = value.trim();
if !v.is_empty() {
m = m.with_venue_name(v);
any = true;
}
}
}
if any { Some(m) } else { None }
}
fn map_service_address(a: &SAddress) -> Option<MAddress> {
let any = a.line1.is_some()
|| a.line2.is_some()
|| a.city.is_some()
|| a.state.is_some()
|| a.postal_code.is_some()
|| a.country.is_some();
if !any {
return None;
}
let mut m = MAddress::new();
if let Some(v) = a.line1.as_deref() {
m = m.with_line1(v);
}
if let Some(v) = a.line2.as_deref() {
m = m.with_line2(v);
}
if let Some(v) = a.city.as_deref() {
m = m.with_city(v);
}
if let Some(v) = a.state.as_deref() {
m = m.with_county(v); }
if let Some(v) = a.postal_code.as_deref() {
m = m.with_postcode(v);
}
if let Some(v) = a.country.as_deref() {
m = m.with_country(v);
}
Some(m)
}
fn identifier_to_event_id(id: &Identifier) -> Option<MEventId> {
MEventId::new(map_identifier_scheme(&id.identifier_type, &id.system), id.value.trim())
}
pub fn map_identifier_scheme(t: &IdentifierType, system: &str) -> MScheme {
let sys = system.to_ascii_lowercase();
if sys.contains("eventbrite") {
return MScheme::Eventbrite;
}
if sys.contains("meetup") {
return MScheme::Meetup;
}
if sys.contains("ticketmaster") {
return MScheme::Ticketmaster;
}
if sys.contains("songkick") {
return MScheme::Songkick;
}
if sys.contains("bandsintown") {
return MScheme::Bandsintown;
}
if sys.contains("facebook") || sys.contains("fb.com") {
return MScheme::Facebook;
}
if sys.contains("lu.ma") || sys.contains("luma") {
return MScheme::Luma;
}
if sys.contains("google") || sys.contains("calendar.google") {
return MScheme::GoogleCalendar;
}
if sys.contains("wikidata") {
return MScheme::Wikidata;
}
if sys.contains("ical") || sys.contains("vcal") {
return MScheme::ICalendarUid;
}
MScheme::Other(match t {
IdentifierType::BookingNumber => "BookingNumber".into(),
IdentifierType::ConfirmationCode => "ConfirmationCode".into(),
IdentifierType::TicketNumber => "TicketNumber".into(),
IdentifierType::EncounterId => "EncounterId".into(),
IdentifierType::TransactionId => "TransactionId".into(),
IdentifierType::ExternalRef => "ExternalRef".into(),
IdentifierType::Tax => "Tax".into(),
IdentifierType::Other => "Other".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{
identifier::Identifier, Event, EventStatus, Location, Party, PartyKind, Place,
VirtualLocation,
};
use chrono::{TimeZone, Utc};
fn svc_event(name: &str) -> Event {
Event::new(name, Utc.with_ymd_and_hms(2026, 6, 1, 9, 0, 0).unwrap())
}
#[test]
fn round_trip_basic() {
let mut e = svc_event("Annual Conference");
e.event_type = EventType::Conference;
e.event_status = EventStatus::Scheduled;
let m = to_matcher_event(&e);
assert_eq!(m.name.as_deref(), Some("Annual Conference"));
assert_eq!(m.category, Some(MCategory::ConferenceEvent));
assert_eq!(m.event_status, Some(MStatus::EventScheduled));
assert!(m.start_date.is_some());
}
#[test]
fn place_location_maps_to_venue_name_and_geo() {
let mut e = svc_event("Gig");
e.location = vec![Location::Place(Place {
id: None,
name: "Greek Theatre".into(),
address: None,
latitude: Some(37.8730),
longitude: Some(-122.2547),
url: None,
})];
let m = to_matcher_event(&e);
let loc = m.location.as_ref().unwrap();
assert_eq!(loc.venue_name.as_deref(), Some("Greek Theatre"));
assert_eq!(loc.latitude, Some(37.8730));
}
#[test]
fn virtual_location_maps_to_virtual_url() {
let mut e = svc_event("Webinar");
e.location = vec![Location::Virtual(VirtualLocation {
name: Some("Zoom".into()),
url: "https://example.zoom.us/j/123".into(),
})];
let m = to_matcher_event(&e);
assert_eq!(
m.location.as_ref().unwrap().virtual_url.as_deref(),
Some("https://example.zoom.us/j/123")
);
}
#[test]
fn organizer_takes_first_party_name() {
let mut e = svc_event("Event");
e.organizers = vec![Party {
kind: PartyKind::Organization,
id: None,
name: "Cal Performances".into(),
email: None,
url: None,
}];
let m = to_matcher_event(&e);
assert_eq!(m.organizer.as_deref(), Some("Cal Performances"));
}
#[test]
fn identifier_eventbrite_recognised_via_system_uri() {
let mut e = svc_event("Event");
e.identifiers.push(Identifier {
use_type: None,
identifier_type: IdentifierType::ExternalRef,
system: "https://eventbrite.com/event-id".into(),
value: "123456789".into(),
assigner: None,
});
let m = to_matcher_event(&e);
assert_eq!(m.event_ids.len(), 1);
assert_eq!(m.event_ids[0].scheme, MScheme::Eventbrite);
assert_eq!(m.event_ids[0].value, "123456789");
}
}