use http::{Method, StatusCode};
use crate::{
caldav::{CalendarComponent, supported_calendar_component_set},
dav::WebDavError,
names,
requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
xmlutils::XmlNode,
};
pub struct CreateCalendar<'a> {
path: &'a str,
display_name: Option<&'a str>,
colour: Option<&'a str>,
order: Option<i32>,
component_set: Option<&'a [CalendarComponent]>,
}
impl<'a> CreateCalendar<'a> {
#[must_use]
pub fn new(path: &'a str) -> Self {
Self {
path,
display_name: None,
colour: None,
order: None,
component_set: None,
}
}
#[must_use]
pub fn with_display_name(mut self, name: &'a str) -> Self {
self.display_name = Some(name);
self
}
#[must_use]
pub fn with_colour(mut self, colour: &'a str) -> Self {
self.colour = Some(colour);
self
}
#[must_use]
pub fn with_order(mut self, order: i32) -> Self {
self.order = Some(order);
self
}
#[must_use]
pub fn with_components(mut self, components: &'a [CalendarComponent]) -> Self {
self.component_set = Some(components);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateCalendarResponse {
pub created: bool,
pub etag: Option<String>,
}
impl DavRequest for CreateCalendar<'_> {
type Response = CreateCalendarResponse;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
let mut resourcetype = XmlNode::new(&names::RESOURCETYPE);
resourcetype.children = vec![
XmlNode::new(&names::COLLECTION),
XmlNode::new(&names::CALENDAR),
];
let mut prop_children: Vec<XmlNode<'_>> = vec![resourcetype];
if let Some(name) = self.display_name {
prop_children.push(XmlNode::new(&names::DISPLAY_NAME).with_text(name));
}
if let Some(colour) = self.colour {
prop_children.push(XmlNode::new(&names::CALENDAR_COLOUR).with_text(colour));
}
if let Some(components) = self.component_set {
prop_children.push(supported_calendar_component_set(components));
}
let order_string = self.order.map(|o| o.to_string());
if let Some(ref order_str) = order_string {
prop_children.push(XmlNode::new(&names::CALENDAR_ORDER).with_text(order_str));
}
let mut prop = XmlNode::new(&names::PROP);
prop.children = prop_children;
let set = XmlNode::new(&names::SET).with_children(vec![prop]);
let mkcol = XmlNode::new(&names::MKCOL).with_children(vec![set]);
Ok(PreparedRequest {
method: Method::from_bytes(b"MKCOL")?,
path: self.path.to_string(),
body: mkcol.render_node(),
headers: vec![xml_content_type_header()],
})
}
fn parse_response(
&self,
parts: &http::response::Parts,
_body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
let created = parts.status == StatusCode::CREATED || parts.status.is_success();
if !created {
return Err(ParseResponseError::BadStatusCode(parts.status));
}
let etag = parts
.headers
.get("etag")
.map(|hv| std::str::from_utf8(hv.as_bytes()))
.transpose()?
.map(str::to_string);
Ok(CreateCalendarResponse { created, etag })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prepare_request_minimal() {
let req = CreateCalendar::new("/calendars/work/");
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"MKCOL").unwrap());
assert_eq!(prepared.path, "/calendars/work/");
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_display_name() {
let req = CreateCalendar::new("/calendars/work/").with_display_name("Work Calendar");
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"<D:displayname>Work Calendar</D:displayname>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_colour() {
let req = CreateCalendar::new("/calendars/work/").with_colour("#FF0000");
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:A="http://apple.com/ns/ical/" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"<A:calendar-color>#FF0000</A:calendar-color>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_order() {
let req = CreateCalendar::new("/calendars/work/").with_order(10);
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:A="http://apple.com/ns/ical/" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"<A:calendar-order>10</A:calendar-order>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_components() {
let req = CreateCalendar::new("/calendars/work/")
.with_components(&[CalendarComponent::VEvent, CalendarComponent::VTodo]);
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"<C:supported-calendar-component-set>"#,
r#"<C:comp name="VEVENT"/><C:comp name="VTODO"/>"#,
r#"</C:supported-calendar-component-set>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_all_options() {
let req = CreateCalendar::new("/calendars/work/")
.with_display_name("Work Calendar")
.with_colour("#FF0000")
.with_order(10)
.with_components(&[CalendarComponent::VEvent]);
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:A="http://apple.com/ns/ical/" xmlns:C="urn:ietf:params:xml:ns:caldav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><C:calendar/></D:resourcetype>"#,
r#"<D:displayname>Work Calendar</D:displayname>"#,
r#"<A:calendar-color>#FF0000</A:calendar-color>"#,
r#"<C:supported-calendar-component-set><C:comp name="VEVENT"/></C:supported-calendar-component-set>"#,
r#"<A:calendar-order>10</A:calendar-order>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_parse_response_created() {
let req = CreateCalendar::new("/calendars/work/");
let response = http::Response::builder()
.status(StatusCode::CREATED)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"").unwrap();
assert!(result.created);
assert_eq!(result.etag, None);
}
#[test]
fn test_parse_response_created_with_etag() {
let req = CreateCalendar::new("/calendars/work/");
let response = http::Response::builder()
.status(StatusCode::CREATED)
.header("etag", "\"123abc\"")
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"").unwrap();
assert!(result.created);
assert_eq!(result.etag, Some("\"123abc\"".to_string()));
}
#[test]
fn test_parse_response_bad_status() {
let req = CreateCalendar::new("/calendars/work/");
let response = http::Response::builder()
.status(StatusCode::FORBIDDEN)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(result.is_err());
}
}