use http::{Method, StatusCode};
use crate::{
dav::WebDavError,
names,
requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
xmlutils::XmlNode,
};
pub struct CreateAddressBook<'a> {
path: &'a str,
display_name: Option<&'a str>,
description: Option<&'a str>,
}
impl<'a> CreateAddressBook<'a> {
#[must_use]
pub fn new(path: &'a str) -> Self {
Self {
path,
display_name: None,
description: 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_description(mut self, description: &'a str) -> Self {
self.description = Some(description);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateAddressBookResponse {
pub created: bool,
pub etag: Option<String>,
}
impl DavRequest for CreateAddressBook<'_> {
type Response = CreateAddressBookResponse;
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::ADDRESSBOOK),
];
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(description) = self.description {
prop_children
.push(XmlNode::new(&names::ADDRESSBOOK_DESCRIPTION).with_text(description));
}
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(CreateAddressBookResponse { created, etag })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prepare_request_minimal() {
let req = CreateAddressBook::new("/addressbooks/contacts/");
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"MKCOL").unwrap());
assert_eq!(prepared.path, "/addressbooks/contacts/");
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_display_name() {
let req =
CreateAddressBook::new("/addressbooks/contacts/").with_display_name("My Contacts");
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>"#,
r#"<D:displayname>My Contacts</D:displayname>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_description() {
let req =
CreateAddressBook::new("/addressbooks/contacts/").with_description("Personal contacts");
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>"#,
r#"<CARD:addressbook-description>Personal contacts</CARD:addressbook-description>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_prepare_request_with_all_options() {
let req = CreateAddressBook::new("/addressbooks/contacts/")
.with_display_name("My Contacts")
.with_description("Personal contacts");
let prepared = req.prepare_request().unwrap();
assert_eq!(
prepared.body,
concat!(
r#"<D:mkcol xmlns:D="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">"#,
r#"<D:set><D:prop>"#,
r#"<D:resourcetype><D:collection/><CARD:addressbook/></D:resourcetype>"#,
r#"<D:displayname>My Contacts</D:displayname>"#,
r#"<CARD:addressbook-description>Personal contacts</CARD:addressbook-description>"#,
r#"</D:prop></D:set>"#,
r#"</D:mkcol>"#,
)
);
}
#[test]
fn test_parse_response_created() {
let req = CreateAddressBook::new("/addressbooks/contacts/");
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 = CreateAddressBook::new("/addressbooks/contacts/");
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 = CreateAddressBook::new("/addressbooks/contacts/");
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());
}
}