use crate::common::property::Property;
use crate::datetime::DateTimeValue;
use crate::error::Result;
use crate::ical::alarm::Alarm;
use crate::ical::recurrence::RecurrenceRule;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub(crate) uid: Option<String>,
pub(crate) dtstamp: Option<DateTimeValue>,
pub(crate) dtstart: Option<DateTimeValue>,
pub(crate) dtend: Option<DateTimeValue>,
pub(crate) summary: Option<String>,
pub(crate) description: Option<String>,
pub(crate) location: Option<String>,
pub(crate) status: Option<String>,
pub(crate) categories: Vec<String>,
pub(crate) priority: Option<u8>,
pub(crate) url: Option<String>,
pub(crate) rrule: Option<RecurrenceRule>,
pub(crate) alarms: Vec<Alarm>,
pub(crate) extra_properties: Vec<Property>,
}
impl Event {
pub fn new() -> Self {
Self {
uid: None,
dtstamp: None,
dtstart: None,
dtend: None,
summary: None,
description: None,
location: None,
status: None,
categories: Vec::new(),
priority: None,
url: None,
rrule: None,
alarms: Vec::new(),
extra_properties: Vec::new(),
}
}
pub fn uid(mut self, uid: impl Into<String>) -> Self {
self.uid = Some(uid.into());
self
}
pub fn summary(mut self, summary: impl Into<String>) -> Self {
self.summary = Some(summary.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
pub fn starts(mut self, dt: &str) -> Self {
self.dtstart = Some(DateTimeValue::parse(dt).expect("invalid start date-time"));
self
}
pub fn starts_dt(mut self, dt: DateTimeValue) -> Self {
self.dtstart = Some(dt);
self
}
pub fn ends(mut self, dt: &str) -> Self {
self.dtend = Some(DateTimeValue::parse(dt).expect("invalid end date-time"));
self
}
pub fn ends_dt(mut self, dt: DateTimeValue) -> Self {
self.dtend = Some(dt);
self
}
pub fn status(mut self, status: impl Into<String>) -> Self {
self.status = Some(status.into());
self
}
pub fn categories(mut self, categories: Vec<String>) -> Self {
self.categories = categories;
self
}
pub fn add_category(mut self, category: impl Into<String>) -> Self {
self.categories.push(category.into());
self
}
pub fn priority(mut self, priority: u8) -> Self {
self.priority = Some(priority);
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn rrule(mut self, rrule: RecurrenceRule) -> Self {
self.rrule = Some(rrule);
self
}
pub fn alarm(mut self, alarm: Alarm) -> Self {
self.alarms.push(alarm);
self
}
pub fn property(mut self, prop: Property) -> Self {
self.extra_properties.push(prop);
self
}
pub fn get_uid(&self) -> Option<&str> {
self.uid.as_deref()
}
pub fn get_summary(&self) -> Option<&str> {
self.summary.as_deref()
}
pub fn get_description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn get_location(&self) -> Option<&str> {
self.location.as_deref()
}
pub fn get_starts(&self) -> Option<&DateTimeValue> {
self.dtstart.as_ref()
}
pub fn get_ends(&self) -> Option<&DateTimeValue> {
self.dtend.as_ref()
}
pub fn get_status(&self) -> Option<&str> {
self.status.as_deref()
}
pub fn get_categories(&self) -> &[String] {
&self.categories
}
pub fn get_priority(&self) -> Option<u8> {
self.priority
}
pub fn get_url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn get_rrule(&self) -> Option<&RecurrenceRule> {
self.rrule.as_ref()
}
pub fn get_alarms(&self) -> &[Alarm] {
&self.alarms
}
pub fn get_extra_properties(&self) -> &[Property] {
&self.extra_properties
}
pub(crate) fn to_properties(&self) -> Vec<Property> {
let mut props = Vec::new();
let uid = self
.uid
.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
props.push(Property::new("UID", uid));
if let Some(ref dtstamp) = self.dtstamp {
props.push(dtstamp.to_property("DTSTAMP"));
} else {
#[cfg(feature = "chrono")]
{
let now = chrono::Utc::now();
let dt = DateTimeValue::from_chrono_utc(now);
props.push(dt.to_property("DTSTAMP"));
}
#[cfg(not(feature = "chrono"))]
{
props.push(Property::new("DTSTAMP", "19700101T000000Z"));
}
}
if let Some(ref dt) = self.dtstart {
props.push(dt.to_property("DTSTART"));
}
if let Some(ref dt) = self.dtend {
props.push(dt.to_property("DTEND"));
}
if let Some(ref s) = self.summary {
props.push(Property::new("SUMMARY", escape_text(s)));
}
if let Some(ref s) = self.description {
props.push(Property::new("DESCRIPTION", escape_text(s)));
}
if let Some(ref s) = self.location {
props.push(Property::new("LOCATION", escape_text(s)));
}
if let Some(ref s) = self.status {
props.push(Property::new("STATUS", s));
}
if !self.categories.is_empty() {
props.push(Property::new(
"CATEGORIES",
self.categories
.iter()
.map(|c| escape_text(c))
.collect::<Vec<_>>()
.join(","),
));
}
if let Some(p) = self.priority {
props.push(Property::new("PRIORITY", p.to_string()));
}
if let Some(ref u) = self.url {
props.push(Property::new("URL", u));
}
if let Some(ref rrule) = self.rrule {
props.push(Property::new("RRULE", rrule.to_string()));
}
props.extend(self.extra_properties.clone());
props
}
pub(crate) fn from_properties(props: Vec<Property>, alarms: Vec<Alarm>) -> Result<Self> {
let mut event = Event::new();
event.alarms = alarms;
let mut extra = Vec::new();
for prop in props {
match prop.name.as_str() {
"UID" => event.uid = Some(prop.value.clone()),
"DTSTAMP" => event.dtstamp = Some(DateTimeValue::from_property(&prop)?),
"DTSTART" => event.dtstart = Some(DateTimeValue::from_property(&prop)?),
"DTEND" => event.dtend = Some(DateTimeValue::from_property(&prop)?),
"SUMMARY" => event.summary = Some(unescape_text(&prop.value)),
"DESCRIPTION" => event.description = Some(unescape_text(&prop.value)),
"LOCATION" => event.location = Some(unescape_text(&prop.value)),
"STATUS" => event.status = Some(prop.value.clone()),
"CATEGORIES" => {
event.categories = prop
.value
.split(',')
.map(|s| unescape_text(s.trim()))
.collect();
}
"PRIORITY" => {
event.priority = prop.value.parse().ok();
}
"URL" => event.url = Some(prop.value.clone()),
"RRULE" => event.rrule = Some(RecurrenceRule::parse(&prop.value)?),
_ => extra.push(prop),
}
}
event.extra_properties = extra;
Ok(event)
}
}
impl Default for Event {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn escape_text(s: &str) -> String {
s.replace('\\', "\\\\")
.replace(';', "\\;")
.replace(',', "\\,")
.replace('\n', "\\n")
}
pub(crate) fn unescape_text(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.peek() {
Some('n') | Some('N') => {
result.push('\n');
chars.next();
}
Some('\\') => {
result.push('\\');
chars.next();
}
Some(';') => {
result.push(';');
chars.next();
}
Some(',') => {
result.push(',');
chars.next();
}
_ => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_roundtrip() {
let original = "Hello\\World; with, commas\nand newlines";
let escaped = escape_text(original);
let unescaped = unescape_text(&escaped);
assert_eq!(unescaped, original);
}
#[test]
fn event_builder() {
let event = Event::new()
.summary("Team Standup")
.location("Room 42")
.starts("2026-03-15T09:00:00")
.ends("2026-03-15T09:30:00");
assert_eq!(event.get_summary(), Some("Team Standup"));
assert_eq!(event.get_location(), Some("Room 42"));
assert!(event.get_starts().is_some());
assert!(event.get_ends().is_some());
}
}