mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Migrate propfind and report to rustical_xml
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2510,6 +2510,7 @@ dependencies = [
|
||||
"quick-xml",
|
||||
"rustical_dav",
|
||||
"rustical_store",
|
||||
"rustical_xml",
|
||||
"serde",
|
||||
"sha2",
|
||||
"strum",
|
||||
@@ -2535,6 +2536,7 @@ dependencies = [
|
||||
"quick-xml",
|
||||
"rustical_dav",
|
||||
"rustical_store",
|
||||
"rustical_xml",
|
||||
"serde",
|
||||
"strum",
|
||||
"thiserror 2.0.8",
|
||||
@@ -2602,6 +2604,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rstest",
|
||||
"rstest_reuse",
|
||||
"rustical_xml",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror 2.0.8",
|
||||
|
||||
@@ -26,3 +26,4 @@ rustical_dav = { workspace = true }
|
||||
rustical_store = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
rustical_xml.workspace = true
|
||||
|
||||
@@ -13,16 +13,16 @@ use rustical_dav::{
|
||||
xml::{multistatus::ResponseElement, MultistatusElement, PropElement, PropfindType},
|
||||
};
|
||||
use rustical_store::{auth::User, CalendarObject, CalendarStore};
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, href+)>
|
||||
pub struct CalendarMultigetRequest {
|
||||
#[serde(flatten)]
|
||||
prop: PropfindType,
|
||||
href: Vec<String>,
|
||||
pub(crate) struct CalendarMultigetRequest {
|
||||
#[xml(ty = "untagged")]
|
||||
pub(crate) prop: PropfindType,
|
||||
#[xml(flatten)]
|
||||
pub(crate) href: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn get_objects_calendar_multiget<C: CalendarStore + ?Sized>(
|
||||
@@ -78,7 +78,10 @@ pub async fn handle_calendar_multiget<C: CalendarStore + ?Sized>(
|
||||
PropfindType::Propname => {
|
||||
vec!["propname".to_owned()]
|
||||
}
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rustical_dav::{
|
||||
resource::{CommonPropertiesProp, EitherProp, Resource},
|
||||
xml::{MultistatusElement, PropElement, PropfindType},
|
||||
};
|
||||
use rustical_store::{auth::User, CalendarObject, CalendarStore};
|
||||
use serde::Deserialize;
|
||||
use rustical_store::{auth::User, calendar::UtcDateTime, CalendarObject, CalendarStore};
|
||||
use rustical_xml::{Unit, XmlDeserialize};
|
||||
|
||||
use crate::{
|
||||
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||
@@ -14,70 +15,57 @@ use crate::{
|
||||
|
||||
// TODO: Implement all the other filters
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
struct TimeRangeElement {
|
||||
#[serde(
|
||||
rename = "@start",
|
||||
deserialize_with = "rustical_store::calendar::deserialize_utc_datetime",
|
||||
default
|
||||
)]
|
||||
start: Option<DateTime<Utc>>,
|
||||
#[serde(
|
||||
rename = "@end",
|
||||
deserialize_with = "rustical_store::calendar::deserialize_utc_datetime",
|
||||
default
|
||||
)]
|
||||
end: Option<DateTime<Utc>>,
|
||||
pub(crate) struct TimeRangeElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) start: Option<UtcDateTime>,
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) end: Option<UtcDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
struct ParamFilterElement {
|
||||
is_not_defined: Option<()>,
|
||||
is_not_defined: Option<Unit>,
|
||||
text_match: Option<TextMatchElement>,
|
||||
|
||||
#[serde(rename = "@name")]
|
||||
#[xml(ty = "attr")]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
struct TextMatchElement {
|
||||
#[serde(rename = "@collation")]
|
||||
#[xml(ty = "attr")]
|
||||
collation: String,
|
||||
#[serde(rename = "@negate-collation")]
|
||||
#[xml(ty = "attr")]
|
||||
negate_collation: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
struct PropFilterElement {
|
||||
is_not_defined: Option<()>,
|
||||
pub(crate) struct PropFilterElement {
|
||||
is_not_defined: Option<Unit>,
|
||||
time_range: Option<TimeRangeElement>,
|
||||
text_match: Option<TextMatchElement>,
|
||||
#[serde(default)]
|
||||
#[xml(flatten)]
|
||||
param_filter: Vec<ParamFilterElement>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||
struct CompFilterElement {
|
||||
is_not_defined: Option<()>,
|
||||
time_range: Option<TimeRangeElement>,
|
||||
#[serde(default)]
|
||||
prop_filter: Vec<PropFilterElement>,
|
||||
#[serde(default)]
|
||||
comp_filter: Vec<CompFilterElement>,
|
||||
pub(crate) struct CompFilterElement {
|
||||
pub(crate) is_not_defined: Option<Unit>,
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
#[xml(flatten)]
|
||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||
|
||||
#[serde(rename = "@name")]
|
||||
name: String,
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
impl CompFilterElement {
|
||||
@@ -146,14 +134,14 @@ impl CompFilterElement {
|
||||
if let Some(time_range) = &self.time_range {
|
||||
if let Some(start) = &time_range.start {
|
||||
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) {
|
||||
if start > &first_occurence.utc() {
|
||||
if start.deref() > &first_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(end) = &time_range.end {
|
||||
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
|
||||
if end < &last_occurence.utc() {
|
||||
if end.deref() < &last_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -164,12 +152,11 @@ impl CompFilterElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||
struct FilterElement {
|
||||
comp_filter: CompFilterElement,
|
||||
pub(crate) struct FilterElement {
|
||||
pub(crate) comp_filter: CompFilterElement,
|
||||
}
|
||||
|
||||
impl FilterElement {
|
||||
@@ -178,15 +165,14 @@ impl FilterElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)>
|
||||
pub struct CalendarQueryRequest {
|
||||
#[serde(flatten)]
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType,
|
||||
filter: Option<FilterElement>,
|
||||
timezone: Option<String>,
|
||||
pub(crate) filter: Option<FilterElement>,
|
||||
pub(crate) timezone: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_objects_calendar_query<C: CalendarStore + ?Sized>(
|
||||
@@ -220,7 +206,10 @@ pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
||||
PropfindType::Propname => {
|
||||
vec!["propname".to_owned()]
|
||||
}
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use actix_web::{
|
||||
use calendar_multiget::{handle_calendar_multiget, CalendarMultigetRequest};
|
||||
use calendar_query::{handle_calendar_query, CalendarQueryRequest};
|
||||
use rustical_store::{auth::User, CalendarStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::{handle_sync_collection, SyncCollectionRequest};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -14,17 +14,8 @@ mod calendar_multiget;
|
||||
mod calendar_query;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PropQuery {
|
||||
Allprop,
|
||||
Prop,
|
||||
Propname,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ReportRequest {
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum ReportRequest {
|
||||
CalendarMultiget(CalendarMultigetRequest),
|
||||
CalendarQuery(CalendarQueryRequest),
|
||||
SyncCollection(SyncCollectionRequest),
|
||||
@@ -43,7 +34,7 @@ pub async fn route_report_calendar<C: CalendarStore + ?Sized>(
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let request: ReportRequest = quick_xml::de::from_str(&body)?;
|
||||
let request = ReportRequest::parse_str(&body)?;
|
||||
|
||||
Ok(match request.clone() {
|
||||
ReportRequest::CalendarQuery(cal_query) => {
|
||||
@@ -81,3 +72,66 @@ pub async fn route_report_calendar<C: CalendarStore + ?Sized>(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement};
|
||||
use rustical_dav::xml::{PropElement, PropfindType, Propname};
|
||||
use rustical_store::calendar::UtcDateTime;
|
||||
use rustical_xml::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_xml_sync_collection() {
|
||||
let report_request = ReportRequest::parse_str(
|
||||
r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<CAL:calendar-query xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav">
|
||||
<prop>
|
||||
<getetag />
|
||||
</prop>
|
||||
<CAL:filter>
|
||||
<CAL:comp-filter name="VCALENDAR">
|
||||
<CAL:comp-filter name="VEVENT">
|
||||
<CAL:time-range start="20240924T143437Z" />
|
||||
</CAL:comp-filter>
|
||||
</CAL:comp-filter>
|
||||
</CAL:filter>
|
||||
</CAL:calendar-query>"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
report_request,
|
||||
ReportRequest::CalendarQuery(CalendarQueryRequest {
|
||||
prop: PropfindType::Prop(PropElement {
|
||||
prop: vec![Propname {
|
||||
name: "getetag".to_owned()
|
||||
}]
|
||||
}),
|
||||
filter: Some(FilterElement {
|
||||
comp_filter: CompFilterElement {
|
||||
is_not_defined: None,
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
is_not_defined: None,
|
||||
time_range: Some(TimeRangeElement {
|
||||
start: Some(
|
||||
<UtcDateTime as Value>::deserialize("20240924T143437Z")
|
||||
.unwrap()
|
||||
),
|
||||
end: None
|
||||
}),
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
name: "VEVENT".to_owned()
|
||||
}],
|
||||
name: "VCALENDAR".to_owned()
|
||||
}
|
||||
}),
|
||||
timezone: None,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,49 @@ use rustical_store::{
|
||||
synctoken::{format_synctoken, parse_synctoken},
|
||||
CalendarStore,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::{Value, XmlDeserialize};
|
||||
|
||||
use crate::{
|
||||
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||
Error,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
enum SyncLevel {
|
||||
#[serde(rename = "1")]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum SyncLevel {
|
||||
One,
|
||||
Infinity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
impl Value for SyncLevel {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlDeError> {
|
||||
Ok(match val {
|
||||
"1" => Self::One,
|
||||
"Infinity" => Self::Infinity,
|
||||
// TODO: proper error
|
||||
_ => return Err(rustical_xml::XmlDeError::UnknownError),
|
||||
})
|
||||
}
|
||||
fn serialize(&self) -> String {
|
||||
match self {
|
||||
SyncLevel::One => "1",
|
||||
SyncLevel::Infinity => "Infinity",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
|
||||
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
||||
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
||||
pub struct SyncCollectionRequest {
|
||||
sync_token: String,
|
||||
sync_level: SyncLevel,
|
||||
timezone: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub(crate) struct SyncCollectionRequest {
|
||||
pub(crate) sync_token: String,
|
||||
pub(crate) sync_level: SyncLevel,
|
||||
pub(crate) timezone: Option<String>,
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType,
|
||||
limit: Option<u64>,
|
||||
pub(crate) limit: Option<u64>,
|
||||
}
|
||||
|
||||
pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
||||
@@ -55,7 +70,10 @@ pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
||||
PropfindType::Propname => {
|
||||
vec!["propname".to_owned()]
|
||||
}
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
DavError(#[from] rustical_dav::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
NewXmlDecodeError(#[from] rustical_xml::XmlDeError),
|
||||
|
||||
#[error(transparent)]
|
||||
XmlDecodeError(#[from] quick_xml::DeError),
|
||||
|
||||
@@ -35,6 +38,7 @@ impl actix_web::ResponseError for Error {
|
||||
},
|
||||
Error::DavError(err) => err.status_code(),
|
||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Error::NewXmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -25,3 +25,4 @@ url = { workspace = true }
|
||||
rustical_dav = { workspace = true }
|
||||
rustical_store = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
rustical_xml.workspace = true
|
||||
|
||||
@@ -14,14 +14,14 @@ use rustical_dav::{
|
||||
xml::{PropElement, PropfindType},
|
||||
};
|
||||
use rustical_store::{auth::User, AddressObject, AddressbookStore};
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AddressbookMultigetRequest {
|
||||
#[serde(flatten)]
|
||||
#[xml(ty = "untagged")]
|
||||
prop: PropfindType,
|
||||
#[xml(flatten)]
|
||||
href: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -84,7 +84,10 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore + ?Sized>(
|
||||
PropfindType::Propname => {
|
||||
vec!["propname".to_owned()]
|
||||
}
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -5,24 +5,15 @@ use actix_web::{
|
||||
};
|
||||
use addressbook_multiget::{handle_addressbook_multiget, AddressbookMultigetRequest};
|
||||
use rustical_store::{auth::User, AddressbookStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::{handle_sync_collection, SyncCollectionRequest};
|
||||
use tracing::instrument;
|
||||
|
||||
mod addressbook_multiget;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PropQuery {
|
||||
Allprop,
|
||||
Prop,
|
||||
Propname,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ReportRequest {
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum ReportRequest {
|
||||
AddressbookMultiget(AddressbookMultigetRequest),
|
||||
SyncCollection(SyncCollectionRequest),
|
||||
}
|
||||
@@ -40,7 +31,7 @@ pub async fn route_report_addressbook<AS: AddressbookStore + ?Sized>(
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let request: ReportRequest = quick_xml::de::from_str(&body)?;
|
||||
let request = ReportRequest::parse_str(&body).map_err(crate::Error::NewXmlDecodeError)?;
|
||||
|
||||
Ok(match request.clone() {
|
||||
ReportRequest::AddressbookMultiget(addr_multiget) => {
|
||||
@@ -67,3 +58,40 @@ pub async fn route_report_addressbook<AS: AddressbookStore + ?Sized>(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustical_dav::xml::{PropElement, Propname};
|
||||
use sync_collection::SyncLevel;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_xml_sync_collection() {
|
||||
let report_request = ReportRequest::parse_str(
|
||||
r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<sync-collection xmlns="DAV:">
|
||||
<sync-token />
|
||||
<sync-level>1</sync-level>
|
||||
<prop>
|
||||
<getetag />
|
||||
</prop>
|
||||
</sync-collection>"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
report_request,
|
||||
ReportRequest::SyncCollection(SyncCollectionRequest {
|
||||
sync_token: "".to_owned(),
|
||||
sync_level: SyncLevel::One,
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement {
|
||||
prop: vec![Propname {
|
||||
name: "getetag".to_owned()
|
||||
}]
|
||||
}),
|
||||
limit: None
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,42 @@ use rustical_store::{
|
||||
synctoken::{format_synctoken, parse_synctoken},
|
||||
AddressbookStore,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::{Value, XmlDeserialize};
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
enum SyncLevel {
|
||||
#[serde(rename = "1")]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum SyncLevel {
|
||||
One,
|
||||
Infinity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(dead_code)]
|
||||
impl Value for SyncLevel {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlDeError> {
|
||||
Ok(match val {
|
||||
"1" => Self::One,
|
||||
"Infinity" => Self::Infinity,
|
||||
// TODO: proper error
|
||||
_ => return Err(rustical_xml::XmlDeError::UnknownError),
|
||||
})
|
||||
}
|
||||
fn serialize(&self) -> String {
|
||||
match self {
|
||||
SyncLevel::One => "1",
|
||||
SyncLevel::Infinity => "Infinity",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
|
||||
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
||||
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
||||
pub struct SyncCollectionRequest {
|
||||
sync_token: String,
|
||||
sync_level: SyncLevel,
|
||||
#[serde(flatten)]
|
||||
pub(crate) struct SyncCollectionRequest {
|
||||
pub(crate) sync_token: String,
|
||||
pub(crate) sync_level: SyncLevel,
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType,
|
||||
limit: Option<u64>,
|
||||
pub(crate) limit: Option<u64>,
|
||||
}
|
||||
|
||||
pub async fn handle_sync_collection<AS: AddressbookStore + ?Sized>(
|
||||
@@ -53,7 +67,10 @@ pub async fn handle_sync_collection<AS: AddressbookStore + ?Sized>(
|
||||
PropfindType::Propname => {
|
||||
vec!["propname".to_owned()]
|
||||
}
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::methods::mkcol::route_mkcol;
|
||||
// use super::methods::report::route_report_addressbook;
|
||||
use super::methods::report::route_report_addressbook;
|
||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
||||
use crate::address_object::resource::AddressObjectResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
@@ -242,9 +242,8 @@ impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceServi
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
let mkcol_method = web::method(Method::from_str("MKCOL").unwrap());
|
||||
// TODO: Re-enable REPORT
|
||||
// let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||
res.route(mkcol_method.to(route_mkcol::<AS>))
|
||||
// .route(report_method.to(route_report_addressbook::<AS>))
|
||||
.route(report_method.to(route_report_addressbook::<AS>))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
DavError(#[from] rustical_dav::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
NewXmlDecodeError(#[from] rustical_xml::XmlDeError),
|
||||
|
||||
#[error(transparent)]
|
||||
XmlDecodeError(#[from] quick_xml::DeError),
|
||||
|
||||
@@ -35,6 +38,7 @@ impl actix_web::ResponseError for Error {
|
||||
},
|
||||
Error::DavError(err) => err.status_code(),
|
||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Error::NewXmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -19,6 +19,9 @@ pub enum Error {
|
||||
#[error("prop is read-only")]
|
||||
PropReadOnly,
|
||||
|
||||
#[error(transparent)]
|
||||
NewXmlDeserializationError(#[from] rustical_xml::XmlDeError),
|
||||
|
||||
#[error(transparent)]
|
||||
XmlDeserializationError(#[from] quick_xml::DeError),
|
||||
|
||||
@@ -33,6 +36,7 @@ impl actix_web::error::ResponseError for Error {
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||
Self::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Self::NewXmlDeserializationError(_) => StatusCode::BAD_REQUEST,
|
||||
Self::XmlDeserializationError(_) => StatusCode::BAD_REQUEST,
|
||||
Self::XmlSerializationError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::PropReadOnly => StatusCode::CONFLICT,
|
||||
|
||||
@@ -6,22 +6,16 @@ use crate::resource::Resource;
|
||||
use crate::resource::ResourceService;
|
||||
use crate::xml::MultistatusElement;
|
||||
use crate::xml::PropElement;
|
||||
use crate::xml::PropfindElement;
|
||||
use crate::xml::PropfindType;
|
||||
use crate::Error;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::HttpRequest;
|
||||
use rustical_store::auth::User;
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::de::XmlDocument;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct PropfindElement {
|
||||
#[serde(rename = "$value")]
|
||||
prop: PropfindType,
|
||||
}
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(path_components, req, root_span))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
@@ -48,7 +42,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
|
||||
// A request body is optional. If empty we MUST return all props
|
||||
let propfind: PropfindElement = if !body.is_empty() {
|
||||
quick_xml::de::from_str(&body).map_err(Error::XmlDeserializationError)?
|
||||
PropfindElement::parse_str(&body).map_err(Error::NewXmlDeserializationError)?
|
||||
} else {
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop,
|
||||
@@ -58,7 +52,10 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
let props = match propfind.prop {
|
||||
PropfindType::Allprop => vec!["allprop".to_owned()],
|
||||
PropfindType::Propname => vec!["propname".to_owned()],
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into_inner(),
|
||||
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags
|
||||
.into_iter()
|
||||
.map(|propname| propname.name)
|
||||
.collect(),
|
||||
};
|
||||
let props: Vec<&str> = props.iter().map(String::as_str).collect();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ mod resourcetype;
|
||||
pub mod tag_list;
|
||||
pub mod tag_name;
|
||||
|
||||
pub use propfind::{PropElement, PropfindType};
|
||||
pub use propfind::{PropElement, PropfindElement, PropfindType, Propname};
|
||||
|
||||
use derive_more::derive::From;
|
||||
pub use multistatus::MultistatusElement;
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
use super::TagList;
|
||||
use serde::Deserialize;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use rustical_xml::XmlRootTag;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PropElement {
|
||||
#[serde(flatten)]
|
||||
pub prop: TagList,
|
||||
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)]
|
||||
#[xml(root = b"propfind", ns = b"DAV:")]
|
||||
pub struct PropfindElement {
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub struct PropElement {
|
||||
#[xml(ty = "untagged", flatten)]
|
||||
pub prop: Vec<Propname>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub struct Propname {
|
||||
#[xml(ty = "tag_name")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub enum PropfindType {
|
||||
Propname,
|
||||
Allprop,
|
||||
|
||||
@@ -1,44 +1,10 @@
|
||||
use derive_more::derive::From;
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
use serde::{
|
||||
de::{MapAccess, Visitor},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, From)]
|
||||
pub struct TagList(Vec<String>);
|
||||
|
||||
struct TagListVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for TagListVisitor {
|
||||
type Value = TagList;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("TagList")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut tags = Vec::new();
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
tags.push(key);
|
||||
}
|
||||
Ok(TagList(tags))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TagList {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_map(TagListVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for TagList {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
87
crates/dav/tests/propfind.rs
Normal file
87
crates/dav/tests/propfind.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use rustical_dav::xml::{PropElement, PropfindElement, PropfindType, Propname};
|
||||
use rustical_xml::de::XmlDocument;
|
||||
|
||||
#[test]
|
||||
fn propfind_allprop() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<allprop />
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propfind_propname() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<propname />
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Propname
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propfind_prop() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<prop>
|
||||
<displayname />
|
||||
<color />
|
||||
</prop>
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Prop(PropElement {
|
||||
prop: vec![
|
||||
Propname {
|
||||
name: "displayname".to_owned()
|
||||
},
|
||||
Propname {
|
||||
name: "color".to_owned()
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Example taken from DAVx5
|
||||
#[test]
|
||||
fn propfind_decl() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<prop>
|
||||
<CARD:max-resource-size />
|
||||
<CARD:supported-address-data />
|
||||
<supported-report-set />
|
||||
<n0:getctag xmlns:n0="http://calendarserver.org/ns/" />
|
||||
<sync-token />
|
||||
</prop>
|
||||
</propfind>
|
||||
"#
|
||||
).unwrap();
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use rustical_dav::xml::TagList;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const INPUT: &str = r#"<Document>
|
||||
<prop>
|
||||
<nicename/>
|
||||
<anotherprop/>
|
||||
</prop>
|
||||
</Document>"#;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct PropElement {
|
||||
#[serde(flatten)]
|
||||
tags: TagList,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct Document {
|
||||
prop: PropElement,
|
||||
}
|
||||
|
||||
fn expected_output() -> Document {
|
||||
Document {
|
||||
prop: PropElement {
|
||||
tags: vec!["nicename".to_owned(), "anotherprop".to_owned()].into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tagname_deserialize() {
|
||||
let result: Document = quick_xml::de::from_str(INPUT).unwrap();
|
||||
assert_eq!(result, expected_output());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tagname_serialize() {
|
||||
let mut result = String::new();
|
||||
let mut ser = quick_xml::se::Serializer::new(&mut result);
|
||||
ser.indent(' ', 4);
|
||||
|
||||
let to_serialize = &expected_output();
|
||||
to_serialize.serialize(ser).unwrap();
|
||||
|
||||
assert_eq!(result, INPUT);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ tracing = { workspace = true }
|
||||
pbkdf2 = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
rustical_xml.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::Error;
|
||||
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use derive_more::derive::Deref;
|
||||
use ical::{
|
||||
parser::{ical::component::IcalTimeZone, Component},
|
||||
property::Property,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use rustical_xml::Value;
|
||||
use std::{collections::HashMap, ops::Add};
|
||||
|
||||
lazy_static! {
|
||||
@@ -17,6 +18,24 @@ const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
|
||||
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
||||
const LOCAL_DATE: &str = "%Y%m%d";
|
||||
|
||||
#[derive(Debug, Clone, Deref, PartialEq)]
|
||||
pub struct UtcDateTime(DateTime<Utc>);
|
||||
|
||||
impl Value for UtcDateTime {
|
||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlDeError> {
|
||||
let input = <String as Value>::deserialize(val)?;
|
||||
Ok(Self(
|
||||
NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME)
|
||||
// TODO: proper error
|
||||
.map_err(|_| rustical_xml::XmlDeError::UnknownError)?
|
||||
.and_utc(),
|
||||
))
|
||||
}
|
||||
fn serialize(&self) -> String {
|
||||
format!("{}", self.0.format(UTC_DATE_TIME))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CalDateTime {
|
||||
// Form 1, example: 19980118T230000
|
||||
@@ -29,22 +48,6 @@ pub enum CalDateTime {
|
||||
Date(NaiveDate),
|
||||
}
|
||||
|
||||
pub fn deserialize_utc_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
type Inner = Option<String>;
|
||||
Ok(if let Some(input) = Inner::deserialize(deserializer)? {
|
||||
Some(
|
||||
NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME)
|
||||
.map_err(|err| serde::de::Error::custom(err.to_string()))?
|
||||
.and_utc(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
impl Add<Duration> for CalDateTime {
|
||||
type Output = Self;
|
||||
|
||||
|
||||
@@ -238,16 +238,19 @@ impl Enum {
|
||||
use ::quick_xml::events::Event;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let event = reader.read_event_into(&mut buf)?;
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
loop {
|
||||
let event = reader.read_event_into(&mut buf)?;
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
|
||||
match event {
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
return Self::deserialize(&mut reader, &start, empty);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(::rustical_xml::XmlDeError::UnknownError)
|
||||
match event {
|
||||
Event::Decl(_) => { /* <?xml ... ?> ignore this */ }
|
||||
Event::Comment(_) => { /* ignore this */ }
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
return Self::deserialize(&mut reader, &start, empty);
|
||||
}
|
||||
_ => return Err(::rustical_xml::XmlDeError::UnknownError),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,23 +71,47 @@ impl Field {
|
||||
.expect("tuple structs not supported")
|
||||
}
|
||||
|
||||
/// Field type
|
||||
pub fn ty(&self) -> &syn::Type {
|
||||
fn is_optional(&self) -> bool {
|
||||
if let syn::Type::Path(syn::TypePath { path, .. }) = &self.field.ty {
|
||||
if path.segments.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
let type_ident = &path.segments.first().unwrap().ident;
|
||||
let option: syn::Ident = syn::parse_str("Option").unwrap();
|
||||
return type_ident == &option;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// The type to deserialize to
|
||||
/// - type Option<T> => optional: deserialize with T
|
||||
/// - flatten Vec<T>: deserialize with T
|
||||
/// - deserialize with T
|
||||
pub fn deserializer_type(&self) -> &syn::Type {
|
||||
if self.is_optional() {
|
||||
return get_generic_type(&self.field.ty).unwrap();
|
||||
}
|
||||
if self.attrs.flatten.is_present() {
|
||||
return get_generic_type(&self.field.ty).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)");
|
||||
}
|
||||
&self.field.ty
|
||||
}
|
||||
|
||||
/// Field in the builder struct for the deserializer
|
||||
pub fn builder_field(&self) -> proc_macro2::TokenStream {
|
||||
let field_ident = self.field_ident();
|
||||
let ty = self.ty();
|
||||
let ty = self.deserializer_type();
|
||||
|
||||
let builder_field_type = match (self.attrs.flatten.is_present(), &self.attrs.default) {
|
||||
(_, Some(_default)) => quote! { #ty },
|
||||
(true, None) => {
|
||||
let generic_type = get_generic_type(ty).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)");
|
||||
quote! { Vec<#generic_type> }
|
||||
}
|
||||
(false, None) => quote! { Option<#ty> },
|
||||
let builder_field_type = match (
|
||||
self.attrs.flatten.is_present(),
|
||||
&self.attrs.default,
|
||||
self.is_optional(),
|
||||
) {
|
||||
(_, Some(_default), true) => panic!("default value for Option<T> doesn't make sense"),
|
||||
(_, Some(_default), false) => quote! { #ty },
|
||||
(true, None, true) => panic!("cannot flatten Option<T>"),
|
||||
(true, None, false) => quote! { Vec<#ty> },
|
||||
(false, None, _) => quote! { Option<#ty> },
|
||||
};
|
||||
|
||||
quote! { #field_ident: #builder_field_type }
|
||||
@@ -96,11 +120,16 @@ impl Field {
|
||||
/// Field initialiser in the builder struct for the deserializer
|
||||
pub fn builder_field_init(&self) -> proc_macro2::TokenStream {
|
||||
let field_ident = self.field_ident();
|
||||
let builder_field_initialiser = match (self.attrs.flatten.is_present(), &self.attrs.default)
|
||||
{
|
||||
(_, Some(default)) => quote! { #default() },
|
||||
(true, None) => quote! { vec![] },
|
||||
(false, None) => quote! { None },
|
||||
let builder_field_initialiser = match (
|
||||
self.attrs.flatten.is_present(),
|
||||
&self.attrs.default,
|
||||
self.is_optional(),
|
||||
) {
|
||||
(_, Some(_), true) => unreachable!(),
|
||||
(_, Some(default), false) => quote! { #default() },
|
||||
(true, None, true) => unreachable!(),
|
||||
(true, None, false) => quote! { vec![] },
|
||||
(false, None, _) => quote! { None },
|
||||
};
|
||||
quote! { #field_ident: #builder_field_initialiser }
|
||||
}
|
||||
@@ -111,10 +140,18 @@ impl Field {
|
||||
let builder_value = match (
|
||||
self.attrs.flatten.is_present(),
|
||||
self.attrs.default.is_some(),
|
||||
self.is_optional(),
|
||||
) {
|
||||
(true, _) => quote! { FromIterator::from_iter(builder.#field_ident.into_iter()) },
|
||||
(false, true) => quote! { builder.#field_ident },
|
||||
(false, false) => quote! { builder.#field_ident.expect("todo: handle missing field") },
|
||||
(true, _, true) => unreachable!(),
|
||||
(true, _, false) => {
|
||||
quote! { FromIterator::from_iter(builder.#field_ident.into_iter()) }
|
||||
}
|
||||
(false, true, true) => unreachable!(),
|
||||
(false, true, false) => quote! { builder.#field_ident },
|
||||
(false, false, true) => quote! { builder.#field_ident },
|
||||
(false, false, false) => {
|
||||
quote! { builder.#field_ident.expect("todo: handle missing field") }
|
||||
}
|
||||
};
|
||||
quote! { #field_ident: #builder_value }
|
||||
}
|
||||
@@ -136,22 +173,14 @@ impl Field {
|
||||
|
||||
let field_name = self.xml_name();
|
||||
let field_ident = self.field_ident();
|
||||
let deserializer = self.ty();
|
||||
let deserializer = self.deserializer_type();
|
||||
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
||||
let assignment = match (self.attrs.flatten.is_present(), &self.attrs.default) {
|
||||
(true, _) => {
|
||||
// TODO: Make nicer, watch out with deserializer typing
|
||||
let deserializer = get_generic_type(self.ty()).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)");
|
||||
quote! {
|
||||
builder.#field_ident.push(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?);
|
||||
}
|
||||
quote! { builder.#field_ident.push(#value); }
|
||||
}
|
||||
(false, Some(_default)) => quote! {
|
||||
builder.#field_ident = #value;
|
||||
},
|
||||
(false, None) => quote! {
|
||||
builder.#field_ident = Some(#value);
|
||||
},
|
||||
(false, Some(_default)) => quote! { builder.#field_ident = #value; },
|
||||
(false, None) => quote! { builder.#field_ident = Some(#value); },
|
||||
};
|
||||
|
||||
Some(quote! {
|
||||
@@ -164,20 +193,16 @@ impl Field {
|
||||
return None;
|
||||
}
|
||||
let field_ident = self.field_ident();
|
||||
let deserializer = self.ty();
|
||||
let deserializer = self.deserializer_type();
|
||||
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
||||
|
||||
Some(if self.attrs.flatten.is_present() {
|
||||
let deserializer = get_generic_type(self.ty()).expect("flatten attribute only implemented for explicit generics (rustical_xml will assume the first generic as the inner type)");
|
||||
quote! {
|
||||
_ => {
|
||||
builder.#field_ident.push(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?);
|
||||
}
|
||||
_ => { builder.#field_ident.push(#value); }
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
_ => {
|
||||
builder.#field_ident = Some(<#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)?);
|
||||
}
|
||||
_ => { builder.#field_ident = Some(#value); }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,34 +74,39 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
|
||||
Self: XmlDeserialize,
|
||||
{
|
||||
let mut buf = Vec::new();
|
||||
let event = reader.read_event_into(&mut buf)?;
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
match event {
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
let matches = match (Self::root_ns(), &ns, name) {
|
||||
// Wrong tag
|
||||
(_, _, name) if name.as_ref() != Self::root_tag() => false,
|
||||
// Wrong namespace
|
||||
(Some(root_ns), ns, _) if &ResolveResult::Bound(Namespace(root_ns)) != ns => {
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
if !matches {
|
||||
let root_ns = Self::root_ns();
|
||||
return Err(XmlDeError::InvalidTag(
|
||||
format!("{ns:?}"),
|
||||
String::from_utf8_lossy(name.as_ref()).to_string(),
|
||||
format!("{root_ns:?}"),
|
||||
String::from_utf8_lossy(Self::root_tag()).to_string(),
|
||||
));
|
||||
};
|
||||
loop {
|
||||
let event = reader.read_event_into(&mut buf)?;
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
match event {
|
||||
Event::Decl(_) => { /* <?xml ... ?> ignore this */ }
|
||||
Event::Comment(_) => { /* ignore this */ }
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
let matches = match (Self::root_ns(), &ns, name) {
|
||||
// Wrong tag
|
||||
(_, _, name) if name.as_ref() != Self::root_tag() => false,
|
||||
// Wrong namespace
|
||||
(Some(root_ns), ns, _)
|
||||
if &ResolveResult::Bound(Namespace(root_ns)) != ns =>
|
||||
{
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
if !matches {
|
||||
let root_ns = Self::root_ns();
|
||||
return Err(XmlDeError::InvalidTag(
|
||||
format!("{ns:?}"),
|
||||
String::from_utf8_lossy(name.as_ref()).to_string(),
|
||||
format!("{root_ns:?}"),
|
||||
String::from_utf8_lossy(Self::root_tag()).to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
return Self::deserialize(&mut reader, &start, empty);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(XmlDeError::UnknownError)
|
||||
return Self::deserialize(&mut reader, &start, empty);
|
||||
}
|
||||
_ => return Err(XmlDeError::UnknownError),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ pub use de::XmlRootTag;
|
||||
pub use se::XmlSerialize;
|
||||
pub use value::Value;
|
||||
|
||||
impl<T: XmlDeserialize> XmlDeserialize for Option<T> {
|
||||
fn deserialize<R: BufRead>(
|
||||
reader: &mut quick_xml::NsReader<R>,
|
||||
start: &BytesStart,
|
||||
empty: bool,
|
||||
) -> Result<Self, XmlDeError> {
|
||||
Ok(Some(T::deserialize(reader, start, empty)?))
|
||||
}
|
||||
}
|
||||
// impl<T: XmlDeserialize> XmlDeserialize for Option<T> {
|
||||
// fn deserialize<R: BufRead>(
|
||||
// reader: &mut quick_xml::NsReader<R>,
|
||||
// start: &BytesStart,
|
||||
// empty: bool,
|
||||
// ) -> Result<Self, XmlDeError> {
|
||||
// Ok(Some(T::deserialize(reader, start, empty)?))
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Unit;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
use std::num::{ParseFloatError, ParseIntError};
|
||||
use std::str::FromStr;
|
||||
use std::{convert::Infallible, io::BufRead};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -21,6 +20,21 @@ pub trait Value: Sized {
|
||||
fn deserialize(val: &str) -> Result<Self, XmlDeError>;
|
||||
}
|
||||
|
||||
// impl<T: Value> Value for Option<T> {
|
||||
// fn serialize(&self) -> String {
|
||||
// match self {
|
||||
// Some(inner) => inner.serialize(),
|
||||
// None => "".to_owned(),
|
||||
// }
|
||||
// }
|
||||
// fn deserialize(val: &str) -> Result<Self, XmlDeError> {
|
||||
// match val {
|
||||
// "" => Ok(None),
|
||||
// val => Ok(Some(T::deserialize(val)?)),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
macro_rules! impl_value_parse {
|
||||
($t:ty) => {
|
||||
impl Value for $t {
|
||||
@@ -54,7 +68,7 @@ impl_value_parse!(usize);
|
||||
impl<T: Value> XmlDeserialize for T {
|
||||
fn deserialize<R: BufRead>(
|
||||
reader: &mut quick_xml::NsReader<R>,
|
||||
start: &BytesStart,
|
||||
_start: &BytesStart,
|
||||
empty: bool,
|
||||
) -> Result<Self, XmlDeError> {
|
||||
let mut string = String::new();
|
||||
@@ -64,17 +78,19 @@ impl<T: Value> XmlDeserialize for T {
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Text(text) => {
|
||||
if !start.is_empty() {
|
||||
if !string.is_empty() {
|
||||
// Content already written
|
||||
return Err(XmlDeError::UnsupportedEvent("todo"));
|
||||
return Err(XmlDeError::UnsupportedEvent("content already written"));
|
||||
}
|
||||
string = String::from_utf8_lossy(text.as_ref()).to_string();
|
||||
}
|
||||
Event::End(_) => break,
|
||||
Event::Eof => return Err(XmlDeError::Eof),
|
||||
_ => return Err(XmlDeError::UnsupportedEvent("todo")),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
<Self as Value>::deserialize(&string)
|
||||
Value::deserialize(&string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ fn test_struct_optional_field() {
|
||||
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
|
||||
#[xml(root = b"document")]
|
||||
struct Document {
|
||||
#[xml(default = "Default::default")]
|
||||
child: Option<Child>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user