mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-29 15:29:09 +00:00
Compare commits
16 Commits
v0.9.8
...
b0091d66d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0091d66d1 | ||
|
|
4919514d09 | ||
|
|
602c511c90 | ||
|
|
b208fbaac6 | ||
|
|
eef45ef612 | ||
|
|
dc860a9768 | ||
|
|
dd52fd120c | ||
|
|
bc4c6489ff | ||
|
|
944462ff5e | ||
|
|
d51c44c2e7 | ||
|
|
8bbc03601a | ||
|
|
1d2b90f7c3 | ||
|
|
979a863b2d | ||
|
|
660ac9b121 | ||
|
|
1e9be6c134 | ||
|
|
b6bfb5a620 |
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: Rust CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --verbose
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose --workspace
|
|
||||||
57
.github/workflows/cicd.yml
vendored
Normal file
57
.github/workflows/cicd.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: "CICD"
|
||||||
|
on: [push, pull_request]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: rustup update
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: cargo check
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test Suite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: rustup update
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: cargo test --all-features --verbose --workspace
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: Test Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: rustup update
|
||||||
|
- name: Install tarpaulin
|
||||||
|
run: cargo install cargo-tarpaulin
|
||||||
|
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run tarpaulin
|
||||||
|
run: cargo tarpaulin --workspace --all-features --exclude xml_derive --coveralls ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||||
|
|
||||||
|
lints:
|
||||||
|
name: Lints
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: rustup update
|
||||||
|
- run: rustup component add rustfmt clippy
|
||||||
|
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run cargo fmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: Run cargo clippy
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -5085,6 +5085,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.21.3",
|
"darling 0.21.3",
|
||||||
"heck",
|
"heck",
|
||||||
|
"itertools 0.14.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM rust:1.89-alpine AS chef
|
FROM --platform=$BUILDPLATFORM rust:1.90-alpine AS chef
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|||||||
3
Justfile
3
Justfile
@@ -12,3 +12,6 @@ docs:
|
|||||||
|
|
||||||
docs-dev:
|
docs-dev:
|
||||||
mkdocs serve
|
mkdocs serve
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
cargo tarpaulin --workspace --exclude xml_derive
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use http::{HeaderValue, Method, StatusCode, header};
|
|||||||
use ical::generator::{Emitter, IcalCalendarBuilder};
|
use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
|
use rustical_ical::{CalendarObjectComponent, EventObject};
|
||||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
|
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -83,16 +83,16 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
ical_calendar_builder.add_event(_override.event.clone());
|
ical_calendar_builder.add_event(_override.event.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Todo(TodoObject(todo), overrides) => {
|
CalendarObjectComponent::Todo(todo, overrides) => {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
|
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
|
||||||
for _override in overrides {
|
for _override in overrides {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_todo(_override.0.clone());
|
ical_calendar_builder = ical_calendar_builder.add_todo(_override.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Journal(JournalObject(journal), overrides) => {
|
CalendarObjectComponent::Journal(journal, overrides) => {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
|
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
|
||||||
for _override in overrides {
|
for _override in overrides {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_journal(_override.0.clone());
|
ical_calendar_builder = ical_calendar_builder.add_journal(_override.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
Path((principal, cal_id)): Path<(String, String)>,
|
Path((principal, cal_id)): Path<(String, String)>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
State(resource_service): State<CalendarResourceService<C, S>>,
|
State(resource_service): State<CalendarResourceService<C, S>>,
|
||||||
overwrite: Overwrite,
|
Overwrite(overwrite): Overwrite,
|
||||||
body: String,
|
body: String,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
if !user.is_principal(&principal) {
|
if !user.is_principal(&principal) {
|
||||||
@@ -103,7 +103,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
|
|
||||||
let cal_store = resource_service.cal_store;
|
let cal_store = resource_service.cal_store;
|
||||||
cal_store
|
cal_store
|
||||||
.import_calendar(new_cal, objects, overwrite.is_true())
|
.import_calendar(new_cal, objects, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(StatusCode::OK.into_response())
|
Ok(StatusCode::OK.into_response())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
|
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||||
use rustical_dav::xml::PropfindType;
|
use rustical_dav::xml::PropfindType;
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_store::{CalendarStore, calendar_store::CalendarQuery};
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::XmlDeserialize;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
@@ -17,24 +17,24 @@ pub(crate) struct TimeRangeElement {
|
|||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
|
||||||
struct ParamFilterElement {
|
pub struct ParamFilterElement {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
is_not_defined: Option<()>,
|
pub(crate) is_not_defined: Option<()>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
text_match: Option<TextMatchElement>,
|
pub(crate) text_match: Option<TextMatchElement>,
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct TextMatchElement {
|
pub struct TextMatchElement {
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
collation: String,
|
pub(crate) collation: String,
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
// "yes" or "no", default: "no"
|
// "yes" or "no", default: "no"
|
||||||
negate_condition: Option<String>,
|
pub(crate) negate_condition: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
@@ -42,16 +42,16 @@ struct TextMatchElement {
|
|||||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||||
pub(crate) struct PropFilterElement {
|
pub(crate) struct PropFilterElement {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
is_not_defined: Option<()>,
|
pub(crate) is_not_defined: Option<()>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
time_range: Option<TimeRangeElement>,
|
pub(crate) time_range: Option<TimeRangeElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
text_match: Option<TextMatchElement>,
|
pub(crate) text_match: Option<TextMatchElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
param_filter: Vec<ParamFilterElement>,
|
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
@@ -192,117 +192,3 @@ impl From<&CalendarQueryRequest> for CalendarQuery {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
|
||||||
cal_query: &CalendarQueryRequest,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
store: &C,
|
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
|
||||||
let mut objects = store
|
|
||||||
.calendar_query(principal, cal_id, cal_query.into())
|
|
||||||
.await?;
|
|
||||||
if let Some(filter) = &cal_query.filter {
|
|
||||||
objects.retain(|object| filter.matches(object));
|
|
||||||
}
|
|
||||||
Ok(objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use rustical_dav::xml::PropElement;
|
|
||||||
use rustical_xml::XmlDocument;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
calendar::methods::report::{
|
|
||||||
ReportRequest,
|
|
||||||
calendar_query::{
|
|
||||||
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
|
||||||
PropFilterElement, TextMatchElement,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
calendar_object::{CalendarObjectPropName, CalendarObjectPropWrapperName},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn calendar_query_7_8_7() {
|
|
||||||
const INPUT: &str = r#"
|
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
||||||
<D:prop xmlns:D="DAV:">
|
|
||||||
<D:getetag/>
|
|
||||||
<C:calendar-data/>
|
|
||||||
</D:prop>
|
|
||||||
<C:filter>
|
|
||||||
<C:comp-filter name="VCALENDAR">
|
|
||||||
<C:comp-filter name="VEVENT">
|
|
||||||
<C:prop-filter name="ATTENDEE">
|
|
||||||
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
|
||||||
<C:param-filter name="PARTSTAT">
|
|
||||||
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
|
||||||
</C:param-filter>
|
|
||||||
</C:prop-filter>
|
|
||||||
</C:comp-filter>
|
|
||||||
</C:comp-filter>
|
|
||||||
</C:filter>
|
|
||||||
</C:calendar-query>
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let report = ReportRequest::parse_str(INPUT).unwrap();
|
|
||||||
let calendar_query: CalendarQueryRequest =
|
|
||||||
if let ReportRequest::CalendarQuery(query) = report {
|
|
||||||
query
|
|
||||||
} else {
|
|
||||||
panic!()
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
calendar_query,
|
|
||||||
CalendarQueryRequest {
|
|
||||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
|
||||||
vec![
|
|
||||||
CalendarObjectPropWrapperName::CalendarObject(
|
|
||||||
CalendarObjectPropName::Getetag,
|
|
||||||
),
|
|
||||||
CalendarObjectPropWrapperName::CalendarObject(
|
|
||||||
CalendarObjectPropName::CalendarData(Default::default())
|
|
||||||
),
|
|
||||||
],
|
|
||||||
vec![]
|
|
||||||
)),
|
|
||||||
filter: Some(FilterElement {
|
|
||||||
comp_filter: CompFilterElement {
|
|
||||||
is_not_defined: None,
|
|
||||||
time_range: None,
|
|
||||||
prop_filter: vec![],
|
|
||||||
comp_filter: vec![CompFilterElement {
|
|
||||||
prop_filter: vec![PropFilterElement {
|
|
||||||
name: "ATTENDEE".to_owned(),
|
|
||||||
text_match: Some(TextMatchElement {
|
|
||||||
collation: "i;ascii-casemap".to_owned(),
|
|
||||||
negate_condition: None
|
|
||||||
}),
|
|
||||||
is_not_defined: None,
|
|
||||||
param_filter: vec![ParamFilterElement {
|
|
||||||
is_not_defined: None,
|
|
||||||
name: "PARTSTAT".to_owned(),
|
|
||||||
text_match: Some(TextMatchElement {
|
|
||||||
collation: "i;ascii-casemap".to_owned(),
|
|
||||||
negate_condition: None
|
|
||||||
}),
|
|
||||||
}],
|
|
||||||
time_range: None
|
|
||||||
}],
|
|
||||||
comp_filter: vec![],
|
|
||||||
is_not_defined: None,
|
|
||||||
name: "VEVENT".to_owned(),
|
|
||||||
time_range: None
|
|
||||||
}],
|
|
||||||
name: "VCALENDAR".to_owned()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
timezone: None,
|
|
||||||
timezone_id: None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
120
crates/caldav/src/calendar/methods/report/calendar_query/mod.rs
Normal file
120
crates/caldav/src/calendar/methods/report/calendar_query/mod.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use crate::Error;
|
||||||
|
use rustical_ical::CalendarObject;
|
||||||
|
use rustical_store::CalendarStore;
|
||||||
|
|
||||||
|
mod elements;
|
||||||
|
pub(crate) use elements::*;
|
||||||
|
|
||||||
|
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||||
|
cal_query: &CalendarQueryRequest,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
store: &C,
|
||||||
|
) -> Result<Vec<CalendarObject>, Error> {
|
||||||
|
let mut objects = store
|
||||||
|
.calendar_query(principal, cal_id, cal_query.into())
|
||||||
|
.await?;
|
||||||
|
if let Some(filter) = &cal_query.filter {
|
||||||
|
objects.retain(|object| filter.matches(object));
|
||||||
|
}
|
||||||
|
Ok(objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rustical_dav::xml::PropElement;
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
calendar::methods::report::{
|
||||||
|
ReportRequest,
|
||||||
|
calendar_query::{
|
||||||
|
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
||||||
|
PropFilterElement, TextMatchElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
calendar_object::{CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn calendar_query_7_8_7() {
|
||||||
|
const INPUT: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<D:prop xmlns:D="DAV:">
|
||||||
|
<D:getetag/>
|
||||||
|
<C:calendar-data/>
|
||||||
|
</D:prop>
|
||||||
|
<C:filter>
|
||||||
|
<C:comp-filter name="VCALENDAR">
|
||||||
|
<C:comp-filter name="VEVENT">
|
||||||
|
<C:prop-filter name="ATTENDEE">
|
||||||
|
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
||||||
|
<C:param-filter name="PARTSTAT">
|
||||||
|
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
||||||
|
</C:param-filter>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:filter>
|
||||||
|
</C:calendar-query>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let report = ReportRequest::parse_str(INPUT).unwrap();
|
||||||
|
let calendar_query: CalendarQueryRequest =
|
||||||
|
if let ReportRequest::CalendarQuery(query) = report {
|
||||||
|
query
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
calendar_query,
|
||||||
|
CalendarQueryRequest {
|
||||||
|
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||||
|
vec![
|
||||||
|
CalendarObjectPropWrapperName::CalendarObject(
|
||||||
|
CalendarObjectPropName::Getetag,
|
||||||
|
),
|
||||||
|
CalendarObjectPropWrapperName::CalendarObject(
|
||||||
|
CalendarObjectPropName::CalendarData(Default::default())
|
||||||
|
),
|
||||||
|
],
|
||||||
|
vec![]
|
||||||
|
)),
|
||||||
|
filter: Some(FilterElement {
|
||||||
|
comp_filter: CompFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
time_range: None,
|
||||||
|
prop_filter: vec![],
|
||||||
|
comp_filter: vec![CompFilterElement {
|
||||||
|
prop_filter: vec![PropFilterElement {
|
||||||
|
name: "ATTENDEE".to_owned(),
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
collation: "i;ascii-casemap".to_owned(),
|
||||||
|
negate_condition: None
|
||||||
|
}),
|
||||||
|
is_not_defined: None,
|
||||||
|
param_filter: vec![ParamFilterElement {
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "PARTSTAT".to_owned(),
|
||||||
|
text_match: Some(TextMatchElement {
|
||||||
|
collation: "i;ascii-casemap".to_owned(),
|
||||||
|
negate_condition: None
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
time_range: None
|
||||||
|
}],
|
||||||
|
comp_filter: vec![],
|
||||||
|
is_not_defined: None,
|
||||||
|
name: "VEVENT".to_owned(),
|
||||||
|
time_range: None
|
||||||
|
}],
|
||||||
|
name: "VCALENDAR".to_owned()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
timezone: None,
|
||||||
|
timezone_id: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<response xmlns:CS="http://calendarserver.org/ns/" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns="DAV:" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<response xmlns:CS="http://calendarserver.org/ns/" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns="DAV:" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rustical_store::auth::Principal;
|
|||||||
use rustical_xml::XmlSerializeRoot;
|
use rustical_xml::XmlSerializeRoot;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
// #[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_propfind() {
|
async fn test_propfind() {
|
||||||
let requests: Vec<_> = include_str!("./test_files/propfind.requests")
|
let requests: Vec<_> = include_str!("./test_files/propfind.requests")
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ async fn test_principal_resource(
|
|||||||
simplified_home_set: false,
|
simplified_home_set: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We don't have any calendars here
|
||||||
|
assert!(
|
||||||
|
service
|
||||||
|
.get_members(&("user".to_owned(),))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
service
|
service
|
||||||
.get_resource(&("invalid-user".to_owned(),), true)
|
.get_resource(&("invalid-user".to_owned(),), true)
|
||||||
|
|||||||
@@ -14,16 +14,12 @@ impl IntoResponse for InvalidOverwriteHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Default)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Overwrite {
|
pub struct Overwrite(pub bool);
|
||||||
#[default]
|
|
||||||
T,
|
|
||||||
F,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Overwrite {
|
impl Default for Overwrite {
|
||||||
pub fn is_true(&self) -> bool {
|
fn default() -> Self {
|
||||||
matches!(self, Self::T)
|
Self(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +43,48 @@ impl TryFrom<&[u8]> for Overwrite {
|
|||||||
|
|
||||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
b"T" => Ok(Overwrite::T),
|
b"T" => Ok(Self(true)),
|
||||||
b"F" => Ok(Overwrite::F),
|
b"F" => Ok(Self(false)),
|
||||||
_ => Err(InvalidOverwriteHeader),
|
_ => Err(InvalidOverwriteHeader),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use axum::{extract::FromRequestParts, response::IntoResponse};
|
||||||
|
use http::Request;
|
||||||
|
|
||||||
|
use crate::header::Overwrite;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_overwrite_default() {
|
||||||
|
let request = Request::put("asd").body(()).unwrap();
|
||||||
|
let (mut parts, _) = request.into_parts();
|
||||||
|
let overwrite = Overwrite::from_request_parts(&mut parts, &())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Overwrite(true),
|
||||||
|
overwrite,
|
||||||
|
"By default we want to overwrite!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_overwrite() {
|
||||||
|
assert_eq!(
|
||||||
|
Overwrite(true),
|
||||||
|
Overwrite::try_from(b"T".as_slice()).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Overwrite(false),
|
||||||
|
Overwrite::try_from(b"F".as_slice()).unwrap()
|
||||||
|
);
|
||||||
|
if let Err(err) = Overwrite::try_from(b"aslkdjlad".as_slice()) {
|
||||||
|
let _ = err.into_response();
|
||||||
|
} else {
|
||||||
|
unreachable!("should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub(crate) async fn axum_route_copy<R: ResourceService>(
|
|||||||
State(resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
Overwrite(overwrite): Overwrite,
|
||||||
matched_path: MatchedPath,
|
matched_path: MatchedPath,
|
||||||
header_map: HeaderMap,
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
@@ -39,7 +39,7 @@ pub(crate) async fn axum_route_copy<R: ResourceService>(
|
|||||||
.map_err(|_| crate::Error::Forbidden)?;
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
|
||||||
if resource_service
|
if resource_service
|
||||||
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
|
.copy_resource(&path, &dest_path, &principal, overwrite)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
// Overwritten
|
// Overwritten
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub(crate) async fn axum_route_move<R: ResourceService>(
|
|||||||
State(resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
Overwrite(overwrite): Overwrite,
|
||||||
matched_path: MatchedPath,
|
matched_path: MatchedPath,
|
||||||
header_map: HeaderMap,
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
@@ -39,7 +39,7 @@ pub(crate) async fn axum_route_move<R: ResourceService>(
|
|||||||
.map_err(|_| crate::Error::Forbidden)?;
|
.map_err(|_| crate::Error::Forbidden)?;
|
||||||
|
|
||||||
if resource_service
|
if resource_service
|
||||||
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
|
.copy_resource(&path, &dest_path, &principal, overwrite)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
// Overwritten
|
// Overwritten
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use derive_more::From;
|
|
||||||
use ical::parser::ical::component::IcalJournal;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, From)]
|
|
||||||
pub struct JournalObject(pub IcalJournal);
|
|
||||||
|
|
||||||
impl JournalObject {
|
|
||||||
pub fn get_uid(&self) -> &str {
|
|
||||||
self.0.get_uid()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
mod event;
|
mod event;
|
||||||
mod journal;
|
|
||||||
mod object;
|
mod object;
|
||||||
mod todo;
|
|
||||||
|
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use journal::*;
|
|
||||||
pub use object::*;
|
pub use object::*;
|
||||||
pub use todo::*;
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use super::{EventObject, JournalObject, TodoObject};
|
use super::EventObject;
|
||||||
use crate::CalDateTime;
|
use crate::CalDateTime;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use ical::generator::{Emitter, IcalCalendar};
|
use ical::generator::{Emitter, IcalCalendar};
|
||||||
|
use ical::parser::ical::component::IcalJournal;
|
||||||
use ical::parser::ical::component::IcalTimeZone;
|
use ical::parser::ical::component::IcalTimeZone;
|
||||||
|
use ical::parser::ical::component::IcalTodo;
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -57,8 +59,18 @@ impl rustical_xml::ValueDeserialize for CalendarObjectType {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CalendarObjectComponent {
|
pub enum CalendarObjectComponent {
|
||||||
Event(EventObject, Vec<EventObject>),
|
Event(EventObject, Vec<EventObject>),
|
||||||
Todo(TodoObject, Vec<TodoObject>),
|
Todo(IcalTodo, Vec<IcalTodo>),
|
||||||
Journal(JournalObject, Vec<JournalObject>),
|
Journal(IcalJournal, Vec<IcalJournal>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&CalendarObjectComponent> for CalendarObjectType {
|
||||||
|
fn from(value: &CalendarObjectComponent) -> Self {
|
||||||
|
match value {
|
||||||
|
CalendarObjectComponent::Event(..) => CalendarObjectType::Event,
|
||||||
|
CalendarObjectComponent::Todo(..) => CalendarObjectType::Todo,
|
||||||
|
CalendarObjectComponent::Journal(..) => CalendarObjectType::Journal,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalendarObjectComponent {
|
impl CalendarObjectComponent {
|
||||||
@@ -82,9 +94,9 @@ impl CalendarObjectComponent {
|
|||||||
}
|
}
|
||||||
Ok(Self::Event(main_event, overrides))
|
Ok(Self::Event(main_event, overrides))
|
||||||
}
|
}
|
||||||
fn from_todos(mut todos: Vec<TodoObject>) -> Result<Self, Error> {
|
fn from_todos(mut todos: Vec<IcalTodo>) -> Result<Self, Error> {
|
||||||
let main_todo = todos
|
let main_todo = todos
|
||||||
.extract_if(.., |todo| todo.0.get_recurrence_id().is_none())
|
.extract_if(.., |todo| todo.get_recurrence_id().is_none())
|
||||||
.next()
|
.next()
|
||||||
.expect("there must be one main event");
|
.expect("there must be one main event");
|
||||||
let overrides = todos;
|
let overrides = todos;
|
||||||
@@ -94,7 +106,7 @@ impl CalendarObjectComponent {
|
|||||||
"Calendar object contains multiple UIDs".to_owned(),
|
"Calendar object contains multiple UIDs".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if todo.0.get_recurrence_id().is_none() {
|
if todo.get_recurrence_id().is_none() {
|
||||||
return Err(Error::InvalidData(
|
return Err(Error::InvalidData(
|
||||||
"Calendar object can only contain one main component".to_owned(),
|
"Calendar object can only contain one main component".to_owned(),
|
||||||
));
|
));
|
||||||
@@ -102,9 +114,9 @@ impl CalendarObjectComponent {
|
|||||||
}
|
}
|
||||||
Ok(Self::Todo(main_todo, overrides))
|
Ok(Self::Todo(main_todo, overrides))
|
||||||
}
|
}
|
||||||
fn from_journals(mut journals: Vec<JournalObject>) -> Result<Self, Error> {
|
fn from_journals(mut journals: Vec<IcalJournal>) -> Result<Self, Error> {
|
||||||
let main_journal = journals
|
let main_journal = journals
|
||||||
.extract_if(.., |journal| journal.0.get_recurrence_id().is_none())
|
.extract_if(.., |journal| journal.get_recurrence_id().is_none())
|
||||||
.next()
|
.next()
|
||||||
.expect("there must be one main event");
|
.expect("there must be one main event");
|
||||||
let overrides = journals;
|
let overrides = journals;
|
||||||
@@ -114,7 +126,7 @@ impl CalendarObjectComponent {
|
|||||||
"Calendar object contains multiple UIDs".to_owned(),
|
"Calendar object contains multiple UIDs".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if journal.0.get_recurrence_id().is_none() {
|
if journal.get_recurrence_id().is_none() {
|
||||||
return Err(Error::InvalidData(
|
return Err(Error::InvalidData(
|
||||||
"Calendar object can only contain one main component".to_owned(),
|
"Calendar object can only contain one main component".to_owned(),
|
||||||
));
|
));
|
||||||
@@ -179,16 +191,9 @@ impl CalendarObject {
|
|||||||
.collect(),
|
.collect(),
|
||||||
)?
|
)?
|
||||||
} else if !cal.todos.is_empty() {
|
} else if !cal.todos.is_empty() {
|
||||||
CalendarObjectComponent::from_todos(
|
CalendarObjectComponent::from_todos(cal.todos)?
|
||||||
cal.todos.into_iter().map(|todo| todo.into()).collect(),
|
|
||||||
)?
|
|
||||||
} else if !cal.journals.is_empty() {
|
} else if !cal.journals.is_empty() {
|
||||||
CalendarObjectComponent::from_journals(
|
CalendarObjectComponent::from_journals(cal.journals)?
|
||||||
cal.journals
|
|
||||||
.into_iter()
|
|
||||||
.map(|journal| journal.into())
|
|
||||||
.collect(),
|
|
||||||
)?
|
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::InvalidData(
|
return Err(Error::InvalidData(
|
||||||
"iCalendar component type not supported :(".to_owned(),
|
"iCalendar component type not supported :(".to_owned(),
|
||||||
@@ -215,9 +220,9 @@ impl CalendarObject {
|
|||||||
match &self.data {
|
match &self.data {
|
||||||
// We've made sure before that the first component exists and all components share the
|
// We've made sure before that the first component exists and all components share the
|
||||||
// same UID
|
// same UID
|
||||||
CalendarObjectComponent::Todo(todo, _) => todo.0.get_uid(),
|
CalendarObjectComponent::Todo(todo, _) => todo.get_uid(),
|
||||||
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
|
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
|
||||||
CalendarObjectComponent::Journal(journal, _) => journal.0.get_uid(),
|
CalendarObjectComponent::Journal(journal, _) => journal.get_uid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,11 +242,7 @@ impl CalendarObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_object_type(&self) -> CalendarObjectType {
|
pub fn get_object_type(&self) -> CalendarObjectType {
|
||||||
match self.data {
|
(&self.data).into()
|
||||||
CalendarObjectComponent::Todo(_, _) => CalendarObjectType::Todo,
|
|
||||||
CalendarObjectComponent::Event(_, _) => CalendarObjectType::Event,
|
|
||||||
CalendarObjectComponent::Journal(_, _) => CalendarObjectType::Journal,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use derive_more::From;
|
|
||||||
use ical::parser::ical::component::IcalTodo;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, From)]
|
|
||||||
pub struct TodoObject(pub IcalTodo);
|
|
||||||
|
|
||||||
impl TodoObject {
|
|
||||||
pub fn get_uid(&self) -> &str {
|
|
||||||
self.0.get_uid()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ quote.workspace = true
|
|||||||
proc-macro2.workspace = true
|
proc-macro2.workspace = true
|
||||||
heck.workspace = true
|
heck.workspace = true
|
||||||
darling.workspace = true
|
darling.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
use crate::{Field, attrs::FieldType};
|
use crate::{Field, attrs::FieldType};
|
||||||
@@ -69,6 +70,7 @@ impl NamedStruct {
|
|||||||
self.attrs
|
self.attrs
|
||||||
.ns_prefix
|
.ns_prefix
|
||||||
.iter()
|
.iter()
|
||||||
|
.sorted_by_key(|(_ns, prefix)| prefix.value())
|
||||||
.map(|(ns, prefix)| {
|
.map(|(ns, prefix)| {
|
||||||
let attr_name = if prefix.value().is_empty() {
|
let attr_name = if prefix.value().is_empty() {
|
||||||
"xmlns".to_owned()
|
"xmlns".to_owned()
|
||||||
|
|||||||
Reference in New Issue
Block a user