pub(crate) mod datetime;
mod export;
mod import;
mod json;
mod recur;
use core::{error, fmt};
use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
use serde_json::{Map, Value, json};
use crate::{
component::IcalComponent, ical::Ical, jcal::import::split_component, prop::IcalProp,
value::IcalValue, version::IcalVersion,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IcalJcalError {
NotAComponent,
NotACalendar(String),
}
impl fmt::Display for IcalJcalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotAComponent => {
f.write_str("jCal value is not a [name, [properties], [components]] array")
}
Self::NotACalendar(name) => {
write!(f, "jCal document is a `{name}`, not a `vcalendar`")
}
}
}
}
impl error::Error for IcalJcalError {}
impl Ical<'_> {
pub fn to_jcal(&self) -> Value {
let mut props = vec![json!([
"version",
Map::new(),
"text",
Value::String((*self.version).to_string())
])];
props.extend(self.props.iter().map(IcalProp::to_jcal));
let components: Vec<Value> = self.components.iter().map(IcalComponent::to_jcal).collect();
json!(["vcalendar", props, components])
}
}
impl<'a> Ical<'a> {
pub fn from_jcal(jcal: &'a Value) -> Result<Self, IcalJcalError> {
let (name, props, components) =
split_component(jcal).ok_or(IcalJcalError::NotAComponent)?;
if !name.eq_ignore_ascii_case("vcalendar") {
return Err(IcalJcalError::NotACalendar(name.to_string()));
}
let mut version = IcalVersion::V2_0;
let mut decoded = Vec::new();
for entry in props {
let prop = IcalProp::from_jcal(entry, version);
if prop.name.eq_ignore_ascii_case("VERSION") {
if let IcalValue::Text(text) = &prop.value {
version = text.0.parse().unwrap_or(IcalVersion::V2_0);
}
continue;
}
decoded.push(prop);
}
Ok(Ical {
version,
props: decoded,
components: components
.iter()
.map(|component| IcalComponent::from_jcal(component, version))
.collect(),
})
}
}
#[cfg(test)]
mod tests {
use alloc::{borrow::Cow, vec};
use crate::{
component::{IcalComponent, IcalComponentKind},
ical::Ical,
jcal::IcalJcalError,
param::IcalParam,
prop::{IcalProp, IcalPropKind},
value::{IcalValue, datetime::IcalDateTime, text::IcalText},
version::IcalVersion,
};
fn calendar() -> Ical<'static> {
Ical {
version: IcalVersion::V2_0,
props: vec![IcalProp {
name: IcalPropKind::ProdId.into(),
params: vec![],
value: IcalValue::Text(IcalText(Cow::Borrowed("-//Example//EN"))),
}],
components: vec![IcalComponent {
name: IcalComponentKind::VEvent.into(),
props: vec![
IcalProp {
name: IcalPropKind::Uid.into(),
params: vec![],
value: IcalValue::Text(IcalText(Cow::Borrowed("42@example.com"))),
},
IcalProp {
name: IcalPropKind::DtStart.into(),
params: vec![],
value: IcalValue::DateTime(IcalDateTime(Cow::Borrowed("20260102T120000Z"))),
},
IcalProp {
name: IcalPropKind::Summary.into(),
params: vec![IcalParam::Language(Cow::Borrowed("en"))],
value: IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))),
},
],
components: vec![],
}],
}
}
#[test]
fn round_trips_a_calendar_built_with_no_parser() {
let cal = calendar();
let jcal = cal.to_jcal();
assert_eq!(Ical::from_jcal(&jcal).expect("a vcalendar"), cal);
}
#[test]
fn refuses_a_document_that_is_not_a_vcalendar() {
let jcal = serde_json::json!(["vevent", [], []]);
assert_eq!(
Ical::from_jcal(&jcal),
Err(IcalJcalError::NotACalendar("vevent".into()))
);
}
}