Compare commits

...

19 Commits

Author SHA1 Message Date
Lennart
f850f9b3a3 version 0.9.3 2025-09-02 23:38:41 +02:00
Lennart
0eb8359e26 rewrite combined calendar store in preparation for sharing 2025-09-02 23:30:16 +02:00
Lennart
7d961ea93b frontend: make button descriptions shorter to fit mobile screen 2025-09-02 23:19:15 +02:00
Lennart
375caedec6 update docs 2025-09-02 09:32:28 +02:00
Lennart
2d8d2eb194 Update README.md 2025-09-01 00:29:55 +02:00
Lennart
69e788b363 store: prevent objects from being commited to subscription calendar 2025-08-31 12:40:20 +02:00
Lennart
8ea5321503 Merge branch 'main' into sharing 2025-08-30 13:58:50 +02:00
Lennart
76c03fa4d4 clippy appeasement 2025-08-30 11:56:58 +02:00
Lennart
96b63848f0 version 0.9.2 2025-08-30 00:41:50 +02:00
Lennart
16e5cacefe Docker: Target Rust 1.89
fixes #116
2025-08-30 00:21:41 +02:00
Lennart
3819f623a6 update dependencies 2025-08-30 00:20:51 +02:00
Lennart
c4604d4376 xml: Comprehensive refactoring from byte strings to strings 2025-08-28 18:01:41 +02:00
Lennart K
85787e69bc xml: tiny refactoring 2025-08-28 15:24:19 +02:00
Lennart K
43b4150e28 xml: Change ns_prefix from LitByteStr to LitStr 2025-08-28 15:19:27 +02:00
Lennart K
c38fbe004f clippy appeasement 2025-08-28 15:09:01 +02:00
Lennart
bf5d874481 frontend tweaks 2025-08-28 14:53:17 +02:00
Lennart
c648ed315d version 0.9.1 2025-08-25 19:09:48 +02:00
Lennart
2cf481d4e6 make session cookie samesite=lax by default 2025-08-25 19:09:24 +02:00
Lennart
a4285fb2ac Outsource some Calendar info to CalendarMetadata struct 2025-08-24 12:52:28 +02:00
67 changed files with 584 additions and 677 deletions

176
Cargo.lock generated
View File

@@ -219,9 +219,9 @@ dependencies = [
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.13.2" version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
dependencies = [ dependencies = [
"async-task", "async-task",
"concurrent-queue", "concurrent-queue",
@@ -595,9 +595,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.45" version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -605,9 +605,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.44" version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -1288,7 +1288,7 @@ dependencies = [
"js-sys", "js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.3+wasi-0.2.4",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -1567,7 +1567,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.0", "socket2",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -1911,11 +1911,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [ dependencies = [
"regex-automata 0.1.10", "regex-automata",
] ]
[[package]] [[package]]
@@ -1990,12 +1990,11 @@ dependencies = [
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [ dependencies = [
"overload", "windows-sys 0.52.0",
"winapi",
] ]
[[package]] [[package]]
@@ -2262,12 +2261,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "p256" name = "p256"
version = "0.13.2" version = "0.13.2"
@@ -2519,9 +2512,9 @@ dependencies = [
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@@ -2615,9 +2608,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.8" version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [ dependencies = [
"bytes", "bytes",
"cfg_aliases", "cfg_aliases",
@@ -2626,7 +2619,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2 0.5.10", "socket2",
"thiserror 2.0.16", "thiserror 2.0.16",
"tokio", "tokio",
"tracing", "tracing",
@@ -2635,9 +2628,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.12" version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.3", "getrandom 0.3.3",
@@ -2656,16 +2649,16 @@ dependencies = [
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.5.13" version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.10", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2773,47 +2766,32 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.4.9", "regex-automata",
"regex-syntax 0.8.5", "regex-syntax",
] ]
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.1.10" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax 0.8.5", "regex-syntax",
] ]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.29" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "relative-path" name = "relative-path"
@@ -2934,21 +2912,20 @@ dependencies = [
[[package]] [[package]]
name = "rstest" name = "rstest"
version = "0.25.0" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
dependencies = [ dependencies = [
"futures-timer", "futures-timer",
"futures-util", "futures-util",
"rstest_macros", "rstest_macros",
"rustc_version",
] ]
[[package]] [[package]]
name = "rstest_macros" name = "rstest_macros"
version = "0.25.0" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"glob", "glob",
@@ -3040,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3083,7 +3060,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3123,7 +3100,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3155,7 +3132,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3180,7 +3157,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3205,7 +3182,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3238,7 +3215,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3256,7 +3233,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3271,7 +3248,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3305,7 +3282,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3326,7 +3303,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.9.0" version = "0.9.3"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.16", "thiserror 2.0.16",
@@ -3652,16 +3629,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.0" version = "0.6.0"
@@ -4096,7 +4063,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"slab", "slab",
"socket2 0.6.0", "socket2",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -4443,14 +4410,14 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
"once_cell", "once_cell",
"regex", "regex-automata",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"thread_local", "thread_local",
@@ -4623,11 +4590,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [ dependencies = [
"wit-bindgen-rt", "wit-bindgen",
] ]
[[package]] [[package]]
@@ -4746,22 +4713,6 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.10" version = "0.1.10"
@@ -4771,12 +4722,6 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -5068,13 +5013,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen"
version = "0.39.0" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "writeable" name = "writeable"

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.9.0" version = "0.9.3"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
@@ -48,7 +48,7 @@ rand_core = { version = "0.9", features = ["std"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
regex = "1.10" regex = "1.10"
lazy_static = "1.5" lazy_static = "1.5"
rstest = "0.25" rstest = "0.26"
rstest_reuse = "0.7" rstest_reuse = "0.7"
sha2 = "0.10" sha2 = "0.10"
tokio = { version = "1", features = [ tokio = { version = "1", features = [

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS chef FROM --platform=$BUILDPLATFORM rust:1.89-alpine AS chef
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM ARG BUILDPLATFORM

View File

@@ -4,14 +4,15 @@ a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
RustiCal is under **active development**! RustiCal is under **active development**!
While I've been successfully using RustiCal productively for a few weeks now, While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
you'd still be one of the first testers so expect bugs and rough edges. you'd still be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- also export feature in the frontend - also export feature in the frontend
- Import your existing calendars in the frontend
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5 - **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)

View File

@@ -43,24 +43,24 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0") let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
.gregorian() .gregorian()
.prodid("RustiCal"); .prodid("RustiCal");
if calendar.displayname.is_some() { if let Some(displayname) = calendar.meta.displayname {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(), name: "X-WR-CALNAME".to_owned(),
value: calendar.displayname, value: Some(displayname),
params: None, params: None,
}); });
} }
if calendar.description.is_some() { if let Some(description) = calendar.meta.description {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(), name: "X-WR-CALDESC".to_owned(),
value: calendar.description, value: Some(description),
params: None, params: None,
}); });
} }
if calendar.timezone_id.is_some() { if let Some(timezone_id) = calendar.timezone_id {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(), name: "X-WR-TIMEZONE".to_owned(),
value: calendar.timezone_id, value: Some(timezone_id),
params: None, params: None,
}); });
} }

View File

@@ -10,7 +10,9 @@ use ical::{
parser::{Component, ComponentMut}, parser::{Component, ComponentMut},
}; };
use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::Principal}; use rustical_store::{
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
};
use std::io::BufReader; use std::io::BufReader;
use tracing::instrument; use tracing::instrument;
@@ -83,10 +85,12 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let new_cal = Calendar { let new_cal = Calendar {
principal, principal,
id: cal_id, id: cal_id,
displayname, meta: CalendarMetadata {
order: 0, displayname,
description, order: 0,
color: None, description,
color: None,
},
timezone_id, timezone_id,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,

View File

@@ -8,7 +8,7 @@ use ical::IcalParser;
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -46,7 +46,7 @@ pub struct PropElement {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = b"mkcalendar")] #[xml(root = "mkcalendar")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
struct MkcalendarRequest { struct MkcalendarRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
@@ -54,7 +54,7 @@ struct MkcalendarRequest {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = b"mkcol")] #[xml(root = "mkcol")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
struct MkcolRequest { struct MkcolRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
@@ -112,11 +112,13 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
let calendar = Calendar { let calendar = Calendar {
id: cal_id.to_owned(), id: cal_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
order: request.calendar_order.unwrap_or(0), meta: CalendarMetadata {
displayname: request.displayname, order: request.calendar_order.unwrap_or(0),
displayname: request.displayname,
color: request.calendar_color,
description: request.calendar_description,
},
timezone_id, timezone_id,
color: request.calendar_color,
description: request.calendar_description,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,
subscription_url: request.source.map(|href| href.href), subscription_url: request.source.map(|href| href.href),

View File

@@ -116,19 +116,17 @@ impl CompFilterElement {
// TODO: Implement prop-filter (and comp-filter?) at some point // TODO: Implement prop-filter (and comp-filter?) at some point
if let Some(time_range) = &self.time_range { if let Some(time_range) = &self.time_range {
if let Some(start) = &time_range.start { if let Some(start) = &time_range.start
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) { && let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
if start.deref() > &last_occurence.utc() { && start.deref() > &last_occurence.utc()
return false; {
} return false;
};
} }
if let Some(end) = &time_range.end { if let Some(end) = &time_range.end
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) { && let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
if end.deref() < &first_occurence.utc() { && end.deref() < &first_occurence.utc()
return false; {
} return false;
};
} }
} }
true true
@@ -156,15 +154,15 @@ impl From<&FilterElement> for CalendarQuery {
for comp_filter in comp_filter_vcalendar.comp_filter.iter() { for comp_filter in comp_filter_vcalendar.comp_filter.iter() {
// A calendar object cannot contain both VEVENT and VTODO, so we only have to handle // A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
// whatever we get first // whatever we get first
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") { if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO")
if let Some(time_range) = &comp_filter.time_range { && let Some(time_range) = &comp_filter.time_range
let start = time_range.start.as_ref().map(|start| start.date_naive()); {
let end = time_range.end.as_ref().map(|end| end.date_naive()); let start = time_range.start.as_ref().map(|start| start.date_naive());
return CalendarQuery { let end = time_range.end.as_ref().map(|end| end.date_naive());
time_start: start, return CalendarQuery {
time_end: end, time_start: start,
}; time_end: end,
} };
} }
} }
Default::default() Default::default()

View File

@@ -128,10 +128,10 @@ impl Resource for CalendarResource {
Ok(match prop { Ok(match prop {
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop { CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
CalendarProp::CalendarColor(self.cal.color.clone()) CalendarProp::CalendarColor(self.cal.meta.color.clone())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
CalendarProp::CalendarDescription(self.cal.description.clone()) CalendarProp::CalendarDescription(self.cal.meta.description.clone())
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| { CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
@@ -146,7 +146,7 @@ impl Resource for CalendarResource {
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone()) CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
} }
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
CalendarProp::CalendarOrder(Some(self.cal.order)) CalendarProp::CalendarOrder(Some(self.cal.meta.order))
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into()) CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
@@ -187,11 +187,11 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapper::Calendar(prop) => match prop { CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => { CalendarProp::CalendarColor(color) => {
self.cal.color = color; self.cal.meta.color = color;
Ok(()) Ok(())
} }
CalendarProp::CalendarDescription(description) => { CalendarProp::CalendarDescription(description) => {
self.cal.description = description; self.cal.meta.description = description;
Ok(()) Ok(())
} }
CalendarProp::CalendarTimezone(timezone) => { CalendarProp::CalendarTimezone(timezone) => {
@@ -225,18 +225,18 @@ impl Resource for CalendarResource {
} }
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::CalendarTimezoneId(timezone_id) => { CalendarProp::CalendarTimezoneId(timezone_id) => {
if let Some(tzid) = &timezone_id { if let Some(tzid) = &timezone_id
if !vtimezones_rs::VTIMEZONES.contains_key(tzid) { && !vtimezones_rs::VTIMEZONES.contains_key(tzid)
return Err(rustical_dav::Error::BadRequest(format!( {
"Invalid timezone-id: {tzid}" return Err(rustical_dav::Error::BadRequest(format!(
))); "Invalid timezone-id: {tzid}"
} )));
} }
self.cal.timezone_id = timezone_id; self.cal.timezone_id = timezone_id;
Ok(()) Ok(())
} }
CalendarProp::CalendarOrder(order) => { CalendarProp::CalendarOrder(order) => {
self.cal.order = order.unwrap_or_default(); self.cal.meta.order = order.unwrap_or_default();
Ok(()) Ok(())
} }
CalendarProp::SupportedCalendarComponentSet(comp_set) => { CalendarProp::SupportedCalendarComponentSet(comp_set) => {
@@ -264,11 +264,11 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapperName::Calendar(prop) => match prop { CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
self.cal.color = None; self.cal.meta.color = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
self.cal.description = None; self.cal.meta.description = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => { CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
@@ -277,7 +277,7 @@ impl Resource for CalendarResource {
} }
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
self.cal.order = 0; self.cal.meta.order = 0;
Ok(()) Ok(())
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
@@ -300,10 +300,10 @@ impl Resource for CalendarResource {
} }
fn get_displayname(&self) -> Option<&str> { fn get_displayname(&self) -> Option<&str> {
self.cal.displayname.as_deref() self.cal.meta.displayname.as_deref()
} }
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> { fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
self.cal.displayname = name; self.cal.meta.displayname = name;
Ok(()) Ok(())
} }

View File

@@ -16,13 +16,13 @@ pub enum PrincipalProp {
CalendarUserAddressSet(HrefElement), CalendarUserAddressSet(HrefElement),
// WebDAV Access Control (RFC 3744) // WebDAV Access Control (RFC 3744)
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "principal-URL")]
PrincipalUrl(HrefElement), PrincipalUrl(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMembership(GroupMembership), GroupMembership(GroupMembership),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMemberSet(GroupMemberSet), GroupMemberSet(GroupMemberSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "alternate-URI-set")]
AlternateUriSet, AlternateUriSet,
// #[xml(ns = "rustical_dav::namespace::NS_DAV")] // #[xml(ns = "rustical_dav::namespace::NS_DAV")]
// PrincipalCollectionSet(HrefElement), // PrincipalCollectionSet(HrefElement),

View File

@@ -79,5 +79,5 @@ async fn test_propfind() {
) )
.unwrap(); .unwrap();
let output = response.serialize_to_string().unwrap(); let _output = response.serialize_to_string().unwrap();
} }

View File

@@ -22,7 +22,7 @@ pub struct MkcolAddressbookProp {
resourcetype: Option<Resourcetype>, resourcetype: Option<Resourcetype>,
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
displayname: Option<String>, displayname: Option<String>,
#[xml(rename = b"addressbook-description")] #[xml(rename = "addressbook-description")]
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")] #[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
description: Option<String>, description: Option<String>,
} }
@@ -34,7 +34,7 @@ pub struct PropElement<T: XmlDeserialize> {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = b"mkcol")] #[xml(root = "mkcol")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
struct MkcolRequest { struct MkcolRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]

View File

@@ -8,14 +8,14 @@ use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[xml(unit_variants_ident = "PrincipalPropName")] #[xml(unit_variants_ident = "PrincipalPropName")]
pub enum PrincipalProp { pub enum PrincipalProp {
// WebDAV Access Control (RFC 3744) // WebDAV Access Control (RFC 3744)
#[xml(rename = b"principal-URL")] #[xml(rename = "principal-URL")]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
PrincipalUrl(HrefElement), PrincipalUrl(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMembership(GroupMembership), GroupMembership(GroupMembership),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMemberSet(GroupMemberSet), GroupMemberSet(GroupMemberSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")] #[xml(ns = "rustical_dav::namespace::NS_DAV", rename = "alternate-URI-set")]
AlternateUriSet, AlternateUriSet,
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
PrincipalCollectionSet(HrefElement), PrincipalCollectionSet(HrefElement),

View File

@@ -20,13 +20,13 @@ impl XmlSerialize for UserPrivilegeSet {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
#[derive(XmlSerialize)] #[derive(XmlSerialize)]
pub struct FakeUserPrivilegeSet { pub struct FakeUserPrivilegeSet {
#[xml(rename = b"privilege", flatten)] #[xml(rename = "privilege", flatten)]
privileges: Vec<UserPrivilege>, privileges: Vec<UserPrivilege>,
} }

View File

@@ -60,11 +60,11 @@ pub async fn route_delete<R: ResourceService>(
return Err(crate::Error::PreconditionFailed.into()); return Err(crate::Error::PreconditionFailed.into());
} }
} }
if let Some(if_none_match) = if_none_match { if let Some(if_none_match) = if_none_match
if resource.satisfies_if_none_match(&if_none_match) { && resource.satisfies_if_none_match(&if_none_match)
// Precondition failed {
return Err(crate::Error::PreconditionFailed.into()); // Precondition failed
} return Err(crate::Error::PreconditionFailed.into());
} }
resource_service resource_service
.delete_resource(path_components, !no_trash) .delete_resource(path_components, !no_trash)

View File

@@ -57,7 +57,7 @@ enum Operation<T: XmlDeserialize> {
} }
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
#[xml(root = b"propertyupdate")] #[xml(root = "propertyupdate")]
#[xml(ns = "crate::namespace::NS_DAV")] #[xml(ns = "crate::namespace::NS_DAV")]
struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>); struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>);

View File

@@ -1,12 +1,12 @@
use rustical_xml::{XmlRootTag, XmlSerialize}; use rustical_xml::{XmlRootTag, XmlSerialize};
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(ns = "crate::namespace::NS_DAV", root = b"error")] #[xml(ns = "crate::namespace::NS_DAV", root = "error")]
#[xml(ns_prefix( #[xml(ns_prefix(
crate::namespace::NS_DAV = b"", crate::namespace::NS_DAV = "",
crate::namespace::NS_CARDDAV = b"CARD", crate::namespace::NS_CARDDAV = "CARD",
crate::namespace::NS_CALDAV = b"CAL", crate::namespace::NS_CALDAV = "CAL",
crate::namespace::NS_CALENDARSERVER = b"CS", crate::namespace::NS_CALENDARSERVER = "CS",
crate::namespace::NS_DAVPUSH = b"PUSH" crate::namespace::NS_DAVPUSH = "PUSH"
))] ))]
pub struct ErrorElement<'t, T: XmlSerialize>(#[xml(ty = "untagged")] pub &'t T); pub struct ErrorElement<'t, T: XmlSerialize>(#[xml(ty = "untagged")] pub &'t T);

View File

@@ -22,8 +22,8 @@ pub struct PropstatElement<PropType: XmlSerialize> {
fn xml_serialize_status( fn xml_serialize_status(
status: &StatusCode, status: &StatusCode,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer) XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
@@ -40,13 +40,13 @@ pub enum PropstatWrapper<T: XmlSerialize> {
// <!ELEMENT response (href, ((href*, status)|(propstat+)), // <!ELEMENT response (href, ((href*, status)|(propstat+)),
// responsedescription?) > // responsedescription?) >
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(ns = "crate::namespace::NS_DAV", root = b"response")] #[xml(ns = "crate::namespace::NS_DAV", root = "response")]
#[xml(ns_prefix( #[xml(ns_prefix(
crate::namespace::NS_DAV = b"", crate::namespace::NS_DAV = "",
crate::namespace::NS_CARDDAV = b"CARD", crate::namespace::NS_CARDDAV = "CARD",
crate::namespace::NS_CALDAV = b"CAL", crate::namespace::NS_CALDAV = "CAL",
crate::namespace::NS_CALENDARSERVER = b"CS", crate::namespace::NS_CALENDARSERVER = "CS",
crate::namespace::NS_DAVPUSH = b"PUSH" crate::namespace::NS_DAVPUSH = "PUSH"
))] ))]
pub struct ResponseElement<PropstatType: XmlSerialize> { pub struct ResponseElement<PropstatType: XmlSerialize> {
pub href: String, pub href: String,
@@ -59,8 +59,8 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
fn xml_serialize_optional_status( fn xml_serialize_optional_status(
val: &Option<StatusCode>, val: &Option<StatusCode>,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
XmlSerialize::serialize( XmlSerialize::serialize(
@@ -86,18 +86,18 @@ impl<PT: XmlSerialize> Default for ResponseElement<PT> {
// <!ELEMENT multistatus (response+, responsedescription?) > // <!ELEMENT multistatus (response+, responsedescription?) >
// Extended by sync-token as specified in RFC 6578 // Extended by sync-token as specified in RFC 6578
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(root = b"multistatus", ns = "crate::namespace::NS_DAV")] #[xml(root = "multistatus", ns = "crate::namespace::NS_DAV")]
#[xml(ns_prefix( #[xml(ns_prefix(
crate::namespace::NS_DAV = b"", crate::namespace::NS_DAV = "",
crate::namespace::NS_CARDDAV = b"CARD", crate::namespace::NS_CARDDAV = "CARD",
crate::namespace::NS_CALDAV = b"CAL", crate::namespace::NS_CALDAV = "CAL",
crate::namespace::NS_CALENDARSERVER = b"CS", crate::namespace::NS_CALENDARSERVER = "CS",
crate::namespace::NS_DAVPUSH = b"PUSH" crate::namespace::NS_DAVPUSH = "PUSH"
))] ))]
pub struct MultistatusElement<PropType: XmlSerialize, MemberPropType: XmlSerialize> { pub struct MultistatusElement<PropType: XmlSerialize, MemberPropType: XmlSerialize> {
#[xml(rename = b"response", flatten)] #[xml(rename = "response", flatten)]
pub responses: Vec<ResponseElement<PropType>>, pub responses: Vec<ResponseElement<PropType>>,
#[xml(rename = b"response", flatten)] #[xml(rename = "response", flatten)]
pub member_responses: Vec<ResponseElement<MemberPropType>>, pub member_responses: Vec<ResponseElement<MemberPropType>>,
pub sync_token: Option<String>, pub sync_token: Option<String>,
} }

View File

@@ -7,7 +7,7 @@ use rustical_xml::XmlError;
use rustical_xml::XmlRootTag; use rustical_xml::XmlRootTag;
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"propfind", ns = "crate::namespace::NS_DAV")] #[xml(root = "propfind", ns = "crate::namespace::NS_DAV")]
pub struct PropfindElement<PN: XmlDeserialize> { pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
pub prop: PropfindType<PN>, pub prop: PropfindType<PN>,

View File

@@ -16,7 +16,7 @@ mod tests {
use super::{Resourcetype, ResourcetypeInner}; use super::{Resourcetype, ResourcetypeInner};
#[derive(XmlSerialize, XmlRootTag)] #[derive(XmlSerialize, XmlRootTag)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
resourcetype: Resourcetype, resourcetype: Resourcetype,
} }

View File

@@ -60,7 +60,7 @@ pub struct NresultsElement(#[xml(ty = "text")] u64);
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)> // <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
// <!-- DAV:limit defined in RFC 5323, Section 5.17 --> // <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
// <!-- DAV:prop defined in RFC 4918, Section 14.18 --> // <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
#[xml(ns = "crate::namespace::NS_DAV", root = b"sync-collection")] #[xml(ns = "crate::namespace::NS_DAV", root = "sync-collection")]
pub struct SyncCollectionRequest<PN: XmlDeserialize> { pub struct SyncCollectionRequest<PN: XmlDeserialize> {
#[xml(ns = "crate::namespace::NS_DAV")] #[xml(ns = "crate::namespace::NS_DAV")]
pub sync_token: String, pub sync_token: String,

View File

@@ -13,8 +13,8 @@ impl XmlSerialize for TagList {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns
@@ -22,23 +22,21 @@ impl XmlSerialize for TagList {
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| { .map(|prefix| {
if !prefix.is_empty() { if !prefix.is_empty() {
[*prefix, b":"].concat() format!("{prefix}:")
} else { } else {
Vec::new() String::new()
} }
}); });
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname let qname = tagname
.as_ref() .as_ref()
.map(|tagname| ::quick_xml::name::QName(tagname)); .map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
if let Some(qname) = &qname { if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned()); let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix { if !has_prefix && let Some(ns) = &ns {
if let Some(ns) = &ns { bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
} }
writer.write_event(Event::Start(bytes_start))?; writer.write_event(Event::Start(bytes_start))?;
} }

View File

@@ -25,10 +25,10 @@ pub struct ContentUpdate {
} }
#[derive(XmlSerialize, XmlRootTag, Debug)] #[derive(XmlSerialize, XmlRootTag, Debug)]
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(root = "push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
#[xml(ns_prefix( #[xml(ns_prefix(
rustical_dav::namespace::NS_DAVPUSH = b"", rustical_dav::namespace::NS_DAVPUSH = "",
rustical_dav::namespace::NS_DAV = b"D", rustical_dav::namespace::NS_DAV = "D",
))] ))]
struct PushMessage { struct PushMessage {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]

View File

@@ -35,12 +35,12 @@ pub enum Trigger {
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Clone, Debug)] #[derive(XmlSerialize, XmlDeserialize, PartialEq, Clone, Debug)]
pub struct ContentUpdate( pub struct ContentUpdate(
#[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
); );
#[derive(XmlSerialize, PartialEq, Clone, Debug)] #[derive(XmlSerialize, PartialEq, Clone, Debug)]
pub struct PropertyUpdate( pub struct PropertyUpdate(
#[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
); );
impl XmlDeserialize for PropertyUpdate { impl XmlDeserialize for PropertyUpdate {
@@ -51,8 +51,8 @@ impl XmlDeserialize for PropertyUpdate {
) -> Result<Self, rustical_xml::XmlError> { ) -> Result<Self, rustical_xml::XmlError> {
#[derive(XmlDeserialize, PartialEq, Clone, Debug)] #[derive(XmlDeserialize, PartialEq, Clone, Debug)]
struct FakePropertyUpdate( struct FakePropertyUpdate(
#[xml(rename = b"depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth, #[xml(rename = "depth", ns = "rustical_dav::namespace::NS_DAV")] pub Depth,
#[xml(rename = b"prop", ns = "rustical_dav::namespace::NS_DAV")] pub Unparsed, #[xml(rename = "prop", ns = "rustical_dav::namespace::NS_DAV")] pub Unparsed,
); );
let FakePropertyUpdate(depth, _) = FakePropertyUpdate::deserialize(reader, start, empty)?; let FakePropertyUpdate(depth, _) = FakePropertyUpdate::deserialize(reader, start, empty)?;
Ok(Self(depth)) Ok(Self(depth))

View File

@@ -17,7 +17,7 @@ pub struct WebPushSubscription {
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct SubscriptionPublicKey { pub struct SubscriptionPublicKey {
#[xml(ty = "attr", rename = b"type")] #[xml(ty = "attr", rename = "type")]
pub ty: String, pub ty: String,
#[xml(ty = "text")] #[xml(ty = "text")]
pub key: String, pub key: String,
@@ -33,7 +33,7 @@ pub struct SubscriptionElement {
pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>); pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>);
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = b"push-register")] #[xml(root = "push-register")]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub struct PushRegister { pub struct PushRegister {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]

View File

@@ -17,7 +17,7 @@ export class DeleteButton extends LitElement {
} }
protected render() { protected render() {
let text = this.trash ? 'Move to trash' : 'Delete' let text = this.trash ? 'Trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>` return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
} }

View File

@@ -28,9 +28,9 @@ export class EditAddressbookForm extends LitElement {
override render() { override render() {
return html` return html`
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button> <button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}> <dialog ${ref(this.dialog)}>
<h3>Create addressbook</h3> <h3>Edit addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
Displayname Displayname

View File

@@ -40,9 +40,9 @@ export class EditCalendarForm extends LitElement {
override render() { override render() {
return html` return html`
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button> <button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}> <dialog ${ref(this.dialog)}>
<h3>Create calendar</h3> <h3>Edit calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
Displayname Displayname

View File

@@ -19,7 +19,7 @@ let DeleteButton = class extends i {
return this; return this;
} }
render() { render() {
let text = this.trash ? "Move to trash" : "Delete"; let text = this.trash ? "Trash" : "Delete";
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`; return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
} }
async _onClick(event) { async _onClick(event) {

View File

@@ -27,9 +27,9 @@ let EditAddressbookForm = class extends i {
} }
render() { render() {
return x` return x`
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button> <button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}> <dialog ${n(this.dialog)}>
<h3>Create addressbook</h3> <h3>Edit addressbook</h3>
<form @submit=${this.submit} ${n(this.form)}> <form @submit=${this.submit} ${n(this.form)}>
<label> <label>
Displayname Displayname

View File

@@ -28,9 +28,9 @@ let EditCalendarForm = class extends i {
} }
render() { render() {
return x` return x`
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button> <button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}> <dialog ${n(this.dialog)}>
<h3>Create calendar</h3> <h3>Edit calendar</h3>
<form @submit=${this.submit} ${n(this.form)}> <form @submit=${this.submit} ${n(this.form)}>
<label> <label>
Displayname Displayname

View File

@@ -290,6 +290,7 @@ ul.collection-list {
.color-chip { .color-chip {
background: var(--color); background: var(--color);
grid-area: color-chip; grid-area: color-chip;
margin-left: 8px;
} }
.actions { .actions {
@@ -346,6 +347,17 @@ select {
} }
} }
form {
input[type="text"],
input[type="password"],
input[type="color"],
textarea,
select {
width: 100%;
}
}
svg.icon { svg.icon {
stroke-width: 2px; stroke-width: 2px;
color: var(--text-on-background-color); color: var(--text-on-background-color);

View File

@@ -1,13 +1,13 @@
<h2>{{ user.id }}'s Calendars</h2> <h2>{{ user.id }}'s Calendars</h2>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in calendars %} {% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner"> <div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -15,7 +15,7 @@
</div> </div>
</span> </span>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span> </span>
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
@@ -29,9 +29,9 @@
principal="{{ calendar.principal }}" principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}" cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}" timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}" displayname="{{ calendar.meta.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.description.as_deref().unwrap_or_default() }}" description="{{ calendar.meta.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.color.as_deref().unwrap_or_default() }}" color="{{ calendar.meta.color.as_deref().unwrap_or_default() }}"
components="{{ calendar.components | json }}" components="{{ calendar.components | json }}"
></edit-calendar-form> ></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button> <delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
@@ -51,13 +51,13 @@
<h3>Deleted Calendars</h3> <h3>Deleted Calendars</h3>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in deleted_calendars %} {% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner"> <div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -65,7 +65,7 @@
</div> </div>
</span> </span>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span> </span>
<div class="actions"> <div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"

View File

@@ -4,9 +4,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %} {% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
<h1>{{ calendar.principal }}/{{ name }}</h1> <h1>{{ calendar.principal }}/{{ name }}</h1>
{% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%} {% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<h2>Subscription URL</h2> <h2>Subscription URL</h2>
@@ -25,9 +25,6 @@
{% if let Some(timezone_id) = calendar.timezone_id %} {% if let Some(timezone_id) = calendar.timezone_id %}
<p>{{ timezone_id }}</p> <p>{{ timezone_id }}</p>
{% endif %} {% endif %}
{% if let Some(timezone) = calendar.get_vtimezone() %}
<textarea rows="16" readonly>{{ timezone }}</textarea>
{% endif %}
<pre>{{ calendar|json }}</pre> <pre>{{ calendar|json }}</pre>

View File

@@ -13,6 +13,7 @@ use tower::Service;
#[derive(Clone, RustEmbed, Default)] #[derive(Clone, RustEmbed, Default)]
#[folder = "public/assets"] #[folder = "public/assets"]
#[allow(dead_code)] // Since this is not used with the frontend-dev feature
pub struct Assets; pub struct Assets;
#[derive(Clone, Default)] #[derive(Clone, Default)]

View File

@@ -192,20 +192,19 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
.await .await
.map_err(|e| OidcError::UserInfo(e.to_string()))?; .map_err(|e| OidcError::UserInfo(e.to_string()))?;
if let Some(require_group) = &oidc_config.require_group { if let Some(require_group) = &oidc_config.require_group
if !user_info_claims && !user_info_claims
.additional_claims() .additional_claims()
.groups .groups
.clone() .clone()
.unwrap_or_default() .unwrap_or_default()
.contains(require_group) .contains(require_group)
{ {
return Ok(( return Ok((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"User is not in an authorized group to use RustiCal", "User is not in an authorized group to use RustiCal",
) )
.into_response()); .into_response());
}
} }
let user_id = match oidc_config.claim_userid { let user_id = match oidc_config.claim_userid {

View File

@@ -72,12 +72,11 @@ where
let mut inner = self.inner.clone(); let mut inner = self.inner.clone();
Box::pin(async move { Box::pin(async move {
if let Some(session) = request.extensions().get::<Session>() { if let Some(session) = request.extensions().get::<Session>()
if let Ok(Some(user_id)) = session.get::<String>("user").await { && let Ok(Some(user_id)) = session.get::<String>("user").await
if let Ok(Some(user)) = ap.get_principal(&user_id).await { && let Ok(Some(user)) = ap.get_principal(&user_id).await
request.extensions_mut().insert(user); {
} request.extensions_mut().insert(user);
}
} }
if let Some(auth) = auth_header { if let Some(auth) = auth_header {

View File

@@ -6,13 +6,23 @@ use rustical_ical::CalendarObjectType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar { pub struct CalendarMetadata {
pub principal: String, // Attributes that may be outsourced
pub id: String,
pub displayname: Option<String>, pub displayname: Option<String>,
pub order: i64, pub order: i64,
pub description: Option<String>, pub description: Option<String>,
pub color: Option<String>, pub color: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar {
// Attributes that may be outsourced
#[serde(flatten)]
pub meta: CalendarMetadata,
// Common calendar attributes
pub principal: String,
pub id: String,
pub timezone_id: Option<String>, pub timezone_id: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
pub synctoken: i64, pub synctoken: i64,

View File

@@ -1,282 +1,208 @@
use crate::CalendarStore;
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::Constructor; use std::{collections::HashMap, sync::Arc};
use rustical_ical::CalendarObject;
use std::sync::Arc;
use crate::{ pub trait PrefixedCalendarStore: CalendarStore {
Calendar, CalendarStore, Error, calendar_store::CalendarQuery, const PREFIX: &'static str;
contact_birthday_store::BIRTHDAYS_PREFIX,
};
#[derive(Debug, Constructor)]
pub struct CombinedCalendarStore<CS: CalendarStore, BS: CalendarStore> {
cal_store: Arc<CS>,
birthday_store: Arc<BS>,
} }
impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, BS> { #[derive(Clone)]
fn clone(&self) -> Self { pub struct CombinedCalendarStore {
stores: HashMap<&'static str, Arc<dyn CalendarStore>>,
default: Arc<dyn CalendarStore>,
}
impl CombinedCalendarStore {
pub fn new(default: Arc<dyn CalendarStore>) -> Self {
Self { Self {
cal_store: self.cal_store.clone(), stores: HashMap::new(),
birthday_store: self.birthday_store.clone(), default,
} }
} }
pub fn with_store<CS: PrefixedCalendarStore>(mut self, store: Arc<CS>) -> Self {
let store: Arc<dyn CalendarStore> = store;
self.stores.insert(CS::PREFIX, store);
self
}
fn store_for_id(&self, id: &str) -> Arc<dyn CalendarStore> {
self.stores
.iter()
.find(|&(prefix, _store)| id.starts_with(prefix))
.map(|(_prefix, store)| store.clone())
.unwrap_or(self.default.clone())
}
} }
#[async_trait] #[async_trait]
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> { impl CalendarStore for CombinedCalendarStore {
#[inline] #[inline]
async fn get_calendar( async fn get_calendar(
&self, &self,
principal: &str, principal: &str,
id: &str, id: &str,
show_deleted: bool, show_deleted: bool,
) -> Result<Calendar, Error> { ) -> Result<crate::Calendar, crate::Error> {
if id.starts_with(BIRTHDAYS_PREFIX) { self.store_for_id(id)
self.birthday_store .get_calendar(principal, id, show_deleted)
.get_calendar(principal, id, show_deleted) .await
.await
} else {
self.cal_store
.get_calendar(principal, id, show_deleted)
.await
}
} }
#[inline]
async fn update_calendar( async fn update_calendar(
&self, &self,
principal: String, principal: String,
id: String, id: String,
calendar: Calendar, calendar: crate::Calendar,
) -> Result<(), crate::Error> { ) -> Result<(), crate::Error> {
if id.starts_with(BIRTHDAYS_PREFIX) { self.store_for_id(&id)
self.birthday_store .update_calendar(principal, id, calendar)
.update_calendar(principal, id, calendar) .await
.await
} else {
self.cal_store
.update_calendar(principal, id, calendar)
.await
}
} }
#[inline] async fn insert_calendar(&self, calendar: crate::Calendar) -> Result<(), crate::Error> {
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> { self.store_for_id(&calendar.id)
if calendar.id.starts_with(BIRTHDAYS_PREFIX) { .insert_calendar(calendar)
Err(Error::ReadOnly) .await
} else {
self.cal_store.insert_calendar(calendar).await
}
} }
#[inline] async fn delete_calendar(
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> { &self,
Ok([ principal: &str,
self.cal_store.get_calendars(principal).await?, name: &str,
self.birthday_store.get_calendars(principal).await?, use_trashbin: bool,
] ) -> Result<(), crate::Error> {
.concat()) self.store_for_id(name)
.delete_calendar(principal, name, use_trashbin)
.await
}
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), crate::Error> {
self.store_for_id(name)
.restore_calendar(principal, name)
.await
}
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<rustical_ical::CalendarObject>, Vec<String>, i64), crate::Error> {
self.store_for_id(cal_id)
.sync_changes(principal, cal_id, synctoken)
.await
}
async fn import_calendar(
&self,
calendar: crate::Calendar,
objects: Vec<rustical_ical::CalendarObject>,
merge_existing: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&calendar.id)
.import_calendar(calendar, objects, merge_existing)
.await
}
async fn calendar_query(
&self,
principal: &str,
cal_id: &str,
query: crate::calendar_store::CalendarQuery,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
self.store_for_id(cal_id)
.calendar_query(principal, cal_id, query)
.await
}
async fn restore_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
) -> Result<(), crate::Error> {
self.store_for_id(cal_id)
.restore_object(principal, cal_id, object_id)
.await
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, crate::Error> {
self.store_for_id(cal_id)
.calendar_metadata(principal, cal_id)
.await
}
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
self.store_for_id(cal_id)
.get_objects(principal, cal_id)
.await
}
async fn put_object(
&self,
principal: String,
cal_id: String,
object: rustical_ical::CalendarObject,
overwrite: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&cal_id)
.put_object(principal, cal_id, object, overwrite)
.await
} }
#[inline]
async fn delete_object( async fn delete_object(
&self, &self,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> Result<(), crate::Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { self.store_for_id(cal_id)
self.birthday_store .delete_object(principal, cal_id, object_id, use_trashbin)
.delete_object(principal, cal_id, object_id, use_trashbin) .await
.await
} else {
self.cal_store
.delete_object(principal, cal_id, object_id, use_trashbin)
.await
}
} }
#[inline]
async fn get_object( async fn get_object(
&self, &self,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool, show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<rustical_ical::CalendarObject, crate::Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { self.store_for_id(cal_id)
self.birthday_store .get_object(principal, cal_id, object_id, show_deleted)
.get_object(principal, cal_id, object_id, show_deleted) .await
.await
} else {
self.cal_store
.get_object(principal, cal_id, object_id, show_deleted)
.await
}
} }
#[inline] async fn get_calendars(&self, principal: &str) -> Result<Vec<crate::Calendar>, crate::Error> {
async fn sync_changes( let mut calendars = self.default.get_calendars(principal).await?;
for store in self.stores.values() {
calendars.extend(store.get_calendars(principal).await?);
}
Ok(calendars)
}
async fn get_deleted_calendars(
&self, &self,
principal: &str, principal: &str,
cal_id: &str, ) -> Result<Vec<crate::Calendar>, crate::Error> {
synctoken: i64, let mut calendars = self.default.get_deleted_calendars(principal).await?;
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> { for store in self.stores.values() {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { calendars.extend(store.get_deleted_calendars(principal).await?);
self.birthday_store
.sync_changes(principal, cal_id, synctoken)
.await
} else {
self.cal_store
.sync_changes(principal, cal_id, synctoken)
.await
} }
Ok(calendars)
} }
#[inline]
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.calendar_metadata(principal, cal_id)
.await
} else {
self.cal_store.calendar_metadata(principal, cal_id).await
}
}
#[inline]
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.get_objects(principal, cal_id).await
} else {
self.cal_store.get_objects(principal, cal_id).await
}
}
#[inline]
async fn calendar_query(
&self,
principal: &str,
cal_id: &str,
query: CalendarQuery,
) -> Result<Vec<CalendarObject>, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.calendar_query(principal, cal_id, query)
.await
} else {
self.cal_store
.calendar_query(principal, cal_id, query)
.await
}
}
#[inline]
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), Error> {
if name.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.restore_calendar(principal, name).await
} else {
self.cal_store.restore_calendar(principal, name).await
}
}
#[inline]
async fn import_calendar(
&self,
calendar: Calendar,
objects: Vec<CalendarObject>,
merge_existing: bool,
) -> Result<(), Error> {
if calendar.id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.import_calendar(calendar, objects, merge_existing)
.await
} else {
self.cal_store
.import_calendar(calendar, objects, merge_existing)
.await
}
}
#[inline]
async fn delete_calendar(
&self,
principal: &str,
name: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if name.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.delete_calendar(principal, name, use_trashbin)
.await
} else {
self.cal_store
.delete_calendar(principal, name, use_trashbin)
.await
}
}
#[inline]
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Ok([
self.birthday_store.get_deleted_calendars(principal).await?,
self.cal_store.get_deleted_calendars(principal).await?,
]
.concat())
}
#[inline]
async fn restore_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.restore_object(principal, cal_id, object_id)
.await
} else {
self.cal_store
.restore_object(principal, cal_id, object_id)
.await
}
}
#[inline]
async fn put_object(
&self,
principal: String,
cal_id: String,
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.put_object(principal, cal_id, object, overwrite)
.await
} else {
self.cal_store
.put_object(principal, cal_id, object, overwrite)
.await
}
}
#[inline]
fn is_read_only(&self, cal_id: &str) -> bool { fn is_read_only(&self, cal_id: &str) -> bool {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { self.store_for_id(cal_id).is_read_only(cal_id)
self.birthday_store.is_read_only(cal_id)
} else {
self.cal_store.is_read_only(cal_id)
}
} }
} }

View File

@@ -1,4 +1,7 @@
use crate::{Addressbook, AddressbookStore, Calendar, CalendarStore, Error}; use crate::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
combined_calendar_store::PrefixedCalendarStore,
};
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType}; use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
@@ -10,16 +13,22 @@ pub(crate) const BIRTHDAYS_PREFIX: &str = "_birthdays_";
#[derive(Constructor, Clone)] #[derive(Constructor, Clone)]
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>); pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
fn birthday_calendar(addressbook: Addressbook) -> Calendar { fn birthday_calendar(addressbook: Addressbook) -> Calendar {
Calendar { Calendar {
principal: addressbook.principal, principal: addressbook.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id), id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
displayname: addressbook meta: CalendarMetadata {
.displayname displayname: addressbook
.map(|name| format!("{name} birthdays")), .displayname
order: 0, .map(|name| format!("{name} birthdays")),
description: None, order: 0,
color: None, description: None,
color: None,
},
timezone_id: None, timezone_id: None,
deleted_at: addressbook.deleted_at, deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken, synctoken: addressbook.synctoken,

View File

@@ -22,7 +22,7 @@ pub use secret::Secret;
pub use subscription_store::*; pub use subscription_store::*;
pub use addressbook::Addressbook; pub use addressbook::Addressbook;
pub use calendar::Calendar; pub use calendar::{Calendar, CalendarMetadata};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CollectionOperationInfo { pub enum CollectionOperationInfo {

View File

@@ -433,14 +433,14 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?; Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Some(addressbook) = addressbook { if let Some(addressbook) = addressbook
if let Err(err) = self.sender.try_send(CollectionOperation { && let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Delete, data: CollectionOperationInfo::Delete,
topic: addressbook.push_topic, topic: addressbook.push_topic,
}) { })
error!("Push notification about deleted addressbook failed: {err}"); {
}; error!("Push notification about deleted addressbook failed: {err}");
} };
Ok(()) Ok(())
} }

View File

@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType}; use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken; use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error}; use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
use rustical_store::{CollectionOperation, CollectionOperationInfo}; use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -69,10 +69,12 @@ impl From<CalendarRow> for Calendar {
Self { Self {
principal: value.principal, principal: value.principal,
id: value.id, id: value.id,
displayname: value.displayname, meta: CalendarMetadata {
order: value.order, displayname: value.displayname,
description: value.description, order: value.order,
color: value.color, description: value.description,
color: value.color,
},
timezone_id: value.timezone_id, timezone_id: value.timezone_id,
deleted_at: value.deleted_at, deleted_at: value.deleted_at,
synctoken: value.synctoken, synctoken: value.synctoken,
@@ -159,10 +161,10 @@ impl SqliteCalendarStore {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
calendar.displayname, calendar.meta.displayname,
calendar.description, calendar.meta.description,
calendar.order, calendar.meta.order,
calendar.color, calendar.meta.color,
calendar.subscription_url, calendar.subscription_url,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
@@ -189,10 +191,10 @@ impl SqliteCalendarStore {
WHERE (principal, id) = (?, ?)"#, WHERE (principal, id) = (?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
calendar.displayname, calendar.meta.displayname,
calendar.description, calendar.meta.description,
calendar.order, calendar.meta.order,
calendar.color, calendar.meta.color,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
comp_event, comp_todo, comp_journal, comp_event, comp_todo, comp_journal,
@@ -351,7 +353,6 @@ impl SqliteCalendarStore {
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
// TODO: Prevent objects from being commited to a subscription calendar
let (object_id, ics) = (object.get_id(), object.get_ics()); let (object_id, ics) = (object.get_id(), object.get_ics());
let first_occurence = object let first_occurence = object
@@ -554,14 +555,14 @@ impl CalendarStore for SqliteCalendarStore {
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?; Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Some(cal) = cal { if let Some(cal) = cal
if let Err(err) = self.sender.try_send(CollectionOperation { && let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Delete, data: CollectionOperationInfo::Delete,
topic: cal.push_topic, topic: cal.push_topic,
}) { })
error!("Push notification about deleted calendar failed: {err}"); {
}; error!("Push notification about deleted calendar failed: {err}");
} };
Ok(()) Ok(())
} }
@@ -667,11 +668,16 @@ impl CalendarStore for SqliteCalendarStore {
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
// TODO: Prevent objects from being commited to a subscription calendar
let mut tx = self.db.begin().await.map_err(crate::Error::from)?; let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let object_id = object.get_id().to_owned(); let object_id = object.get_id().to_owned();
let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?;
if calendar.subscription_url.is_some() {
// We cannot commit an object to a subscription calendar
return Err(Error::ReadOnly);
}
Self::_put_object( Self::_put_object(
&mut *tx, &mut *tx,
principal.to_owned(), principal.to_owned(),

View File

@@ -1,11 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use darling::{FromDeriveInput, FromField, FromMeta, FromVariant, util::Flag}; use darling::{FromDeriveInput, FromField, FromMeta, FromVariant, util::Flag};
use syn::{Ident, LitByteStr}; use syn::{Ident, LitStr};
#[derive(Debug, Default, FromMeta, Clone)] #[derive(Debug, Default, FromMeta, Clone)]
pub struct TagAttrs { pub struct TagAttrs {
pub rename: Option<LitByteStr>, pub rename: Option<LitStr>,
pub ns: Option<syn::Path>, pub ns: Option<syn::Path>,
} }
@@ -30,10 +30,10 @@ pub struct EnumAttrs {
#[derive(Default, FromDeriveInput, Clone)] #[derive(Default, FromDeriveInput, Clone)]
#[darling(attributes(xml))] #[darling(attributes(xml))]
pub struct StructAttrs { pub struct StructAttrs {
pub root: Option<LitByteStr>, pub root: Option<LitStr>,
pub ns: Option<syn::Path>, pub ns: Option<syn::Path>,
#[darling(default)] #[darling(default)]
pub ns_prefix: HashMap<syn::Path, LitByteStr>, pub ns_prefix: HashMap<syn::Path, LitStr>,
pub allow_invalid: Flag, pub allow_invalid: Flag,
} }

View File

@@ -35,7 +35,7 @@ impl Field {
} }
/// Field name in XML /// Field name in XML
pub fn xml_name(&self) -> syn::LitByteStr { pub fn xml_name(&self) -> syn::LitStr {
if let Some(rename) = self.attrs.common.rename.to_owned() { if let Some(rename) = self.attrs.common.rename.to_owned() {
rename rename
} else { } else {
@@ -43,7 +43,7 @@ impl Field {
.field_ident() .field_ident()
.as_ref() .as_ref()
.expect("unnamed tag fields need a rename attribute"); .expect("unnamed tag fields need a rename attribute");
syn::LitByteStr::new(ident.to_string().to_kebab_case().as_bytes(), ident.span()) syn::LitStr::new(ident.to_string().to_kebab_case().as_str(), ident.span())
} }
} }
@@ -174,6 +174,8 @@ impl Field {
.map(|ns| quote! { if ns == #ns }); .map(|ns| quote! { if ns == #ns });
let field_name = self.xml_name(); let field_name = self.xml_name();
let b_field_name =
syn::LitByteStr::new(self.xml_name().value().as_bytes(), field_name.span());
let builder_field_ident = self.builder_field_ident(); let builder_field_ident = self.builder_field_ident();
let deserializer = self.deserializer_type(); let deserializer = self.deserializer_type();
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? }; let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
@@ -186,7 +188,7 @@ impl Field {
}; };
Some(quote! { Some(quote! {
(#namespace_match, #field_name) #namespace_condition => { #assignment; } (#namespace_match, #b_field_name) #namespace_condition => { #assignment; }
}) })
} }
@@ -231,6 +233,8 @@ impl Field {
} }
let builder_field_ident = self.builder_field_ident(); let builder_field_ident = self.builder_field_ident();
let field_name = self.xml_name(); let field_name = self.xml_name();
let b_field_name =
syn::LitByteStr::new(self.xml_name().value().as_bytes(), field_name.span());
let value = wrap_option_if_no_default( let value = wrap_option_if_no_default(
quote! { quote! {
@@ -240,7 +244,7 @@ impl Field {
); );
Some(quote! { Some(quote! {
#field_name => { #b_field_name => {
builder.#builder_field_ident = #value; builder.#builder_field_ident = #value;
} }
}) })
@@ -255,7 +259,6 @@ impl Field {
let value = quote! { let value = quote! {
if let ::quick_xml::name::ResolveResult::Bound(ns) = &ns { if let ::quick_xml::name::ResolveResult::Bound(ns) = &ns {
Some(ns.into()) Some(ns.into())
// Some(rustical_xml::ValueDeserialize::deserialize(&String::from_utf8_lossy(ns.0.as_ref()))?)
} else { } else {
None None
} }

View File

@@ -1,16 +1,12 @@
pub(crate) fn get_generic_type(ty: &syn::Type) -> Option<&syn::Type> { pub(crate) fn get_generic_type(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(syn::TypePath { path, .. }) = ty { if let syn::Type::Path(syn::TypePath { path, .. }) = ty
if let Some(seg) = path.segments.last() { && let Some(seg) = path.segments.last()
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { && let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args, args, ..
.. }) = &seg.arguments
}) = &seg.arguments && let Some(syn::GenericArgument::Type(t)) = &args.first()
{ {
if let Some(syn::GenericArgument::Type(t)) = &args.first() { return Some(t);
return Some(t);
}
}
}
} }
None None
} }

View File

@@ -14,13 +14,13 @@ impl Variant {
&self.variant.ident &self.variant.ident
} }
pub fn xml_name(&self) -> syn::LitByteStr { pub fn xml_name(&self) -> syn::LitStr {
self.attrs self.attrs
.common .common
.rename .rename
.to_owned() .to_owned()
.unwrap_or(syn::LitByteStr::new( .unwrap_or(syn::LitStr::new(
self.ident().to_string().to_kebab_case().as_bytes(), self.ident().to_string().to_kebab_case().as_str(),
self.ident().span(), self.ident().span(),
)) ))
} }
@@ -75,6 +75,8 @@ impl Variant {
} }
let ident = self.ident(); let ident = self.ident();
let variant_name = self.xml_name(); let variant_name = self.xml_name();
let b_variant_name =
syn::LitByteStr::new(self.xml_name().value().as_bytes(), variant_name.span());
let deserializer_type = self.deserializer_type(); let deserializer_type = self.deserializer_type();
Some( Some(
@@ -93,7 +95,7 @@ impl Variant {
panic!("tuple variants should contain exactly one element"); panic!("tuple variants should contain exactly one element");
} }
quote! { quote! {
#variant_name => { #b_variant_name => {
let val = Some(<#deserializer_type as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?); let val = Some(<#deserializer_type as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?);
Ok(Self::#ident(val)) Ok(Self::#ident(val))
} }
@@ -104,7 +106,7 @@ impl Variant {
panic!("tuple variants should contain exactly one element"); panic!("tuple variants should contain exactly one element");
} }
quote! { quote! {
#variant_name => { #b_variant_name => {
let val = <#deserializer_type as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?; let val = <#deserializer_type as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?;
Ok(Self::#ident(val)) Ok(Self::#ident(val))
} }
@@ -112,7 +114,7 @@ impl Variant {
} }
(false, Fields::Unit, _) => { (false, Fields::Unit, _) => {
quote! { quote! {
#variant_name => { #b_variant_name => {
// Make sure that content is still consumed // Make sure that content is still consumed
<() as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?; <() as ::rustical_xml::XmlDeserialize>::deserialize(reader, start, empty)?;
Ok(Self::#ident) Ok(Self::#ident)

View File

@@ -111,8 +111,7 @@ impl Enum {
Some(ns) => quote! { Some(#ns) }, Some(ns) => quote! { Some(#ns) },
None => quote! { None }, None => quote! { None },
}; };
let b_xml_name = variant.xml_name().value(); let xml_name = variant.xml_name().value();
let xml_name = String::from_utf8_lossy(&b_xml_name);
let out = quote! {(#ns, #xml_name)}; let out = quote! {(#ns, #xml_name)};
let ident = &variant.variant.ident; let ident = &variant.variant.ident;
@@ -134,8 +133,7 @@ impl Enum {
let str_to_unit_branches = tagged_variants.iter().map(|variant| { let str_to_unit_branches = tagged_variants.iter().map(|variant| {
let ident = &variant.variant.ident; let ident = &variant.variant.ident;
let b_xml_name = variant.xml_name().value(); let xml_name = variant.xml_name().value();
let xml_name = String::from_utf8_lossy(&b_xml_name);
if variant.attrs.prop.is_some() { if variant.attrs.prop.is_some() {
quote! { #xml_name => Ok(Self::#ident (Default::default())) } quote! { #xml_name => Ok(Self::#ident (Default::default())) }
} else { } else {

View File

@@ -16,8 +16,8 @@ impl Enum {
fn serialize( fn serialize(
&self, &self,
ns: Option<::quick_xml::name::Namespace>, ns: Option<::quick_xml::name::Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>, namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &str>,
writer: &mut ::quick_xml::Writer<&mut Vec<u8>> writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
) -> ::std::io::Result<()> { ) -> ::std::io::Result<()> {
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
@@ -25,14 +25,16 @@ impl Enum {
let prefix = ns let prefix = ns
.map(|ns| namespaces.get(&ns)) .map(|ns| namespaces.get(&ns))
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| if !prefix.is_empty() { .map(|prefix| {
[*prefix, b":"].concat() if !prefix.is_empty() {
} else { format!("{prefix}:")
vec![] } else {
}); String::new()
}
});
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname)); let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
const enum_untagged: bool = #enum_untagged; const enum_untagged: bool = #enum_untagged;

View File

@@ -108,8 +108,7 @@ impl Enum {
Some(ns) => quote! { Some(#ns) }, Some(ns) => quote! { Some(#ns) },
None => quote! { None }, None => quote! { None },
}; };
let b_xml_name = variant.xml_name().value(); let xml_name = variant.xml_name().value();
let xml_name = String::from_utf8_lossy(&b_xml_name);
quote! {(#ns, #xml_name)} quote! {(#ns, #xml_name)}
}); });

View File

@@ -33,7 +33,7 @@ impl NamedStruct {
let field_index = field.target_field_index(); let field_index = field.target_field_index();
quote! { quote! {
::quick_xml::events::attributes::Attribute { ::quick_xml::events::attributes::Attribute {
key: ::quick_xml::name::QName(#field_name), key: ::quick_xml::name::QName(#field_name.as_bytes()),
value: ::std::borrow::Cow::from(::rustical_xml::ValueSerialize::serialize(&self.#field_index).into_bytes()) value: ::std::borrow::Cow::from(::rustical_xml::ValueSerialize::serialize(&self.#field_index).into_bytes())
} }
} }
@@ -47,7 +47,7 @@ impl NamedStruct {
let field_index = field.target_field_index(); let field_index = field.target_field_index();
quote! { quote! {
let tag_str = self.#field_index.to_string(); let tag_str = self.#field_index.to_string();
let tag = Some(tag.unwrap_or(tag_str.as_bytes())); let tag = Some(tag.unwrap_or(tag_str.as_str()));
} }
}); });
@@ -70,13 +70,12 @@ impl NamedStruct {
.ns_prefix .ns_prefix
.iter() .iter()
.map(|(ns, prefix)| { .map(|(ns, prefix)| {
let sep = if !prefix.value().is_empty() { let attr_name = if prefix.value().is_empty() {
b":".to_vec() "xmlns".to_owned()
} else { } else {
b"".to_vec() format!("xmlns:{}", prefix.value())
}; };
let attr_name = [b"xmlns".as_ref(), &sep, &prefix.value()].concat(); let a = syn::LitByteStr::new(attr_name.as_bytes(), prefix.span());
let a = syn::LitByteStr::new(&attr_name, prefix.span());
quote! { quote! {
bytes_start.push_attribute((#a.as_ref(), #ns.as_ref())); bytes_start.push_attribute((#a.as_ref(), #ns.as_ref()));
} }
@@ -91,8 +90,8 @@ impl NamedStruct {
fn serialize( fn serialize(
&self, &self,
ns: Option<::quick_xml::name::Namespace>, ns: Option<::quick_xml::name::Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>, namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &str>,
writer: &mut ::quick_xml::Writer<&mut Vec<u8>> writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
) -> ::std::io::Result<()> { ) -> ::std::io::Result<()> {
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
@@ -105,15 +104,15 @@ impl NamedStruct {
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| { .map(|prefix| {
if !prefix.is_empty() { if !prefix.is_empty() {
[*prefix, b":"].concat() format!("{prefix}:")
} else { } else {
Vec::new() String::new()
} }
}); });
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname)); let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
//
if let Some(qname) = &qname { if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned()); let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix { if !has_prefix {

View File

@@ -68,7 +68,7 @@ impl NamedStruct {
.ns_prefix .ns_prefix
.iter() .iter()
.map(|(ns, prefix)| { .map(|(ns, prefix)| {
quote! { (#ns, #prefix.as_ref()) } quote! { (#ns, #prefix) }
}) })
.collect() .collect()
} else { } else {
@@ -77,9 +77,9 @@ impl NamedStruct {
quote! { quote! {
impl #impl_generics ::rustical_xml::XmlRootTag for #ident #type_generics #where_clause { impl #impl_generics ::rustical_xml::XmlRootTag for #ident #type_generics #where_clause {
fn root_tag() -> &'static [u8] { #root } fn root_tag() -> &'static str { #root }
fn root_ns() -> Option<::quick_xml::name::Namespace<'static>> { #ns } fn root_ns() -> Option<::quick_xml::name::Namespace<'static>> { #ns }
fn root_ns_prefixes() -> ::std::collections::HashMap<::quick_xml::name::Namespace<'static>, &'static [u8]> { fn root_ns_prefixes() -> ::std::collections::HashMap<::quick_xml::name::Namespace<'static>, &'static str> {
::std::collections::HashMap::from_iter(vec![ ::std::collections::HashMap::from_iter(vec![
#(#prefixes),* #(#prefixes),*
]) ])

View File

@@ -49,7 +49,7 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
let (ns, name) = reader.resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
let matches = match (Self::root_ns(), &ns, name) { let matches = match (Self::root_ns(), &ns, name) {
// Wrong tag // Wrong tag
(_, _, name) if name.as_ref() != Self::root_tag() => false, (_, _, name) if name.as_ref() != Self::root_tag().as_bytes() => false,
// Wrong namespace // Wrong namespace
(Some(root_ns), ns, _) if &ResolveResult::Bound(root_ns) != ns => false, (Some(root_ns), ns, _) if &ResolveResult::Bound(root_ns) != ns => false,
_ => true, _ => true,
@@ -60,7 +60,7 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
format!("{ns:?}"), format!("{ns:?}"),
String::from_utf8_lossy(name.as_ref()).to_string(), String::from_utf8_lossy(name.as_ref()).to_string(),
format!("{root_ns:?}"), format!("{root_ns:?}"),
String::from_utf8_lossy(Self::root_tag()).to_string(), Self::root_tag().to_owned(),
)); ));
}; };

View File

@@ -23,9 +23,9 @@ pub use xml_derive::PropName;
pub use xml_derive::XmlRootTag; pub use xml_derive::XmlRootTag;
pub trait XmlRootTag { pub trait XmlRootTag {
fn root_tag() -> &'static [u8]; fn root_tag() -> &'static str;
fn root_ns() -> Option<Namespace<'static>>; fn root_ns() -> Option<Namespace<'static>>;
fn root_ns_prefixes() -> HashMap<Namespace<'static>, &'static [u8]>; fn root_ns_prefixes() -> HashMap<Namespace<'static>, &'static str>;
} }
#[derive(Debug)] #[derive(Debug)]

View File

@@ -28,7 +28,7 @@ impl<'a> From<&'a Namespace<'a>> for NamespaceOwned {
} }
impl NamespaceOwned { impl NamespaceOwned {
pub fn as_ref(&self) -> Namespace { pub fn as_ref(&self) -> Namespace<'_> {
Namespace(&self.0) Namespace(&self.0)
} }
} }

View File

@@ -10,8 +10,8 @@ pub trait XmlSerialize {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()>; ) -> std::io::Result<()>;
@@ -22,8 +22,8 @@ impl<T: XmlSerialize> XmlSerialize for Option<T> {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
if let Some(some) = self { if let Some(some) = self {
@@ -60,8 +60,8 @@ impl XmlSerialize for () {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns
@@ -69,20 +69,18 @@ impl XmlSerialize for () {
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| { .map(|prefix| {
if !prefix.is_empty() { if !prefix.is_empty() {
[*prefix, b":"].concat() [*prefix, ":"].concat()
} else { } else {
Vec::new() String::new()
} }
}); });
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| QName(tagname)); let qname = tagname.as_ref().map(|tagname| QName(tagname.as_bytes()));
if let Some(qname) = &qname { if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned()); let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix { if !has_prefix && let Some(ns) = &ns {
if let Some(ns) = &ns { bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
} }
writer.write_event(Event::Empty(bytes_start))?; writer.write_event(Event::Empty(bytes_start))?;
} }

View File

@@ -107,8 +107,8 @@ impl<T: ValueSerialize> XmlSerialize for T {
fn serialize( fn serialize(
&self, &self,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns
@@ -116,20 +116,18 @@ impl<T: ValueSerialize> XmlSerialize for T {
.unwrap_or(None) .unwrap_or(None)
.map(|prefix| { .map(|prefix| {
if !prefix.is_empty() { if !prefix.is_empty() {
[*prefix, b":"].concat() [*prefix, ":"].concat()
} else { } else {
Vec::new() String::new()
} }
}); });
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| QName(tagname)); let qname = tagname.as_ref().map(|tagname| QName(tagname.as_bytes()));
if let Some(qname) = &qname { if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned()); let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix { if !has_prefix && let Some(ns) = &ns {
if let Some(ns) = &ns { bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
} }
writer.write_event(Event::Start(bytes_start))?; writer.write_event(Event::Start(bytes_start))?;
} }

View File

@@ -1,9 +1,9 @@
use rustical_xml::{de::XmlDocument, XmlDeserialize, XmlRootTag}; use rustical_xml::{XmlDeserialize, XmlRootTag, de::XmlDocument};
#[test] #[test]
fn test_struct_tagged_enum() { fn test_struct_tagged_enum() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"propfind")] #[xml(root = "propfind")]
struct Propfind { struct Propfind {
prop: Prop, prop: Prop,
} }
@@ -58,7 +58,7 @@ fn test_struct_tagged_enum() {
#[test] #[test]
fn test_tagged_enum_complex() { fn test_tagged_enum_complex() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"propfind")] #[xml(root = "propfind")]
struct Propfind { struct Propfind {
prop: PropStruct, prop: PropStruct,
} }
@@ -116,7 +116,7 @@ fn test_enum_document() {
#[test] #[test]
fn test_untagged_enum() { fn test_untagged_enum() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
prop: PropElement, prop: PropElement,
} }

View File

@@ -6,7 +6,7 @@ use std::collections::HashSet;
#[test] #[test]
fn test_struct_text_field() { fn test_struct_text_field() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "text")] #[xml(ty = "text")]
text: String, text: String,
@@ -27,7 +27,7 @@ fn test_struct_text_field() {
#[test] #[test]
fn test_struct_document() { fn test_struct_document() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Child, child: Child,
} }
@@ -52,9 +52,9 @@ fn test_struct_document() {
#[test] #[test]
fn test_struct_rename_field() { fn test_struct_rename_field() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(rename = b"ok-wow")] #[xml(rename = "ok-wow")]
child: Child, child: Child,
} }
@@ -78,7 +78,7 @@ fn test_struct_rename_field() {
#[test] #[test]
fn test_struct_optional_field() { fn test_struct_optional_field() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Option<Child>, child: Option<Child>,
} }
@@ -96,9 +96,9 @@ fn test_struct_optional_field() {
#[test] #[test]
fn test_struct_vec() { fn test_struct_vec() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(rename = b"child", flatten)] #[xml(rename = "child", flatten)]
children: Vec<Child>, children: Vec<Child>,
} }
@@ -124,9 +124,9 @@ fn test_struct_vec() {
#[test] #[test]
fn test_struct_set() { fn test_struct_set() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(rename = b"child", flatten)] #[xml(rename = "child", flatten)]
children: HashSet<Child>, children: HashSet<Child>,
} }
@@ -154,7 +154,7 @@ fn test_struct_ns() {
const NS_HELLO: Namespace = Namespace(b"hello"); const NS_HELLO: Namespace = Namespace(b"hello");
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ns = "NS_HELLO")] #[xml(ns = "NS_HELLO")]
child: (), child: (),
@@ -169,7 +169,7 @@ fn test_struct_attr() {
const NS_HELLO: Namespace = Namespace(b"hello"); const NS_HELLO: Namespace = Namespace(b"hello");
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ns = "NS_HELLO")] #[xml(ns = "NS_HELLO")]
child: (), child: (),
@@ -196,7 +196,7 @@ fn test_struct_attr() {
#[test] #[test]
fn test_struct_generics() { fn test_struct_generics() {
#[derive(XmlDeserialize, XmlRootTag)] #[derive(XmlDeserialize, XmlRootTag)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document<T: XmlDeserialize> { struct Document<T: XmlDeserialize> {
#[allow(dead_code)] #[allow(dead_code)]
child: T, child: T,
@@ -217,7 +217,7 @@ fn test_struct_generics() {
#[test] #[test]
fn test_struct_unparsed() { fn test_struct_unparsed() {
#[derive(XmlDeserialize, XmlRootTag)] #[derive(XmlDeserialize, XmlRootTag)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[allow(dead_code)] #[allow(dead_code)]
child: Unparsed, child: Unparsed,
@@ -238,7 +238,7 @@ fn test_struct_unparsed() {
#[test] #[test]
fn test_xml_values() { fn test_xml_values() {
#[derive(XmlDeserialize, XmlRootTag, PartialEq, Debug)] #[derive(XmlDeserialize, XmlRootTag, PartialEq, Debug)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
href: String, href: String,
} }
@@ -262,7 +262,7 @@ fn test_xml_values() {
#[test] #[test]
fn test_xml_cdata() { fn test_xml_cdata() {
#[derive(XmlDeserialize, XmlRootTag, PartialEq, Debug)] #[derive(XmlDeserialize, XmlRootTag, PartialEq, Debug)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "text")] #[xml(ty = "text")]
hello: String, hello: String,
@@ -293,7 +293,7 @@ fn test_xml_cdata() {
#[test] #[test]
fn test_struct_xml_decl() { fn test_struct_xml_decl() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Child, child: Child,
} }
@@ -323,7 +323,7 @@ fn test_struct_xml_decl() {
#[test] #[test]
fn test_struct_tuple() { fn test_struct_tuple() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Child, child: Child,
} }
@@ -348,7 +348,7 @@ fn test_struct_tuple() {
#[test] #[test]
fn test_struct_untagged_ns() { fn test_struct_untagged_ns() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
child: Child, child: Child,

View File

@@ -29,7 +29,7 @@ enum CalendarProp {
#[allow(dead_code)] #[allow(dead_code)]
Getcontenttype(&'static str), Getcontenttype(&'static str),
#[xml(ns = "NS_DAV", rename = b"principal-URL")] #[xml(ns = "NS_DAV", rename = "principal-URL")]
#[allow(dead_code)] #[allow(dead_code)]
PrincipalUrl, PrincipalUrl,
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -36,7 +36,7 @@ fn test_propertyupdate() {
} }
#[derive(XmlDeserialize, XmlRootTag)] #[derive(XmlDeserialize, XmlRootTag)]
#[xml(root = b"propertyupdate")] #[xml(root = "propertyupdate")]
struct PropertyupdateElement<T: XmlDeserialize> { struct PropertyupdateElement<T: XmlDeserialize> {
#[xml(ty = "untagged", flatten)] #[xml(ty = "untagged", flatten)]
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -3,7 +3,7 @@ use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
#[test] #[test]
fn test_struct_value_tagged() { fn test_struct_value_tagged() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"propfind")] #[xml(root = "propfind")]
struct Document { struct Document {
prop: Prop, prop: Prop,
} }

View File

@@ -7,7 +7,7 @@ use xml_derive::XmlDeserialize;
#[test] #[test]
fn test_struct_document() { fn test_struct_document() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Child, child: Child,
} }
@@ -30,7 +30,7 @@ fn test_struct_document() {
#[test] #[test]
fn test_struct_untagged_attr() { fn test_struct_untagged_attr() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "attr")] #[xml(ty = "attr")]
name: String, name: String,
@@ -57,7 +57,7 @@ fn test_struct_untagged_attr() {
#[test] #[test]
fn test_struct_value_tagged() { fn test_struct_value_tagged() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
href: String, href: String,
num: usize, num: usize,
@@ -82,7 +82,7 @@ fn test_struct_value_tagged() {
#[test] #[test]
fn test_struct_value_untagged() { fn test_struct_value_untagged() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
href: String, href: String,
@@ -103,7 +103,7 @@ fn test_struct_value_untagged() {
#[test] #[test]
fn test_struct_vec() { fn test_struct_vec() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(flatten)] #[xml(flatten)]
href: Vec<String>, href: Vec<String>,
@@ -127,7 +127,7 @@ fn test_struct_vec() {
#[test] #[test]
fn test_struct_serialize_with() { fn test_struct_serialize_with() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(serialize_with = "serialize_href")] #[xml(serialize_with = "serialize_href")]
href: String, href: String,
@@ -136,8 +136,8 @@ fn test_struct_serialize_with() {
fn serialize_href( fn serialize_href(
val: &str, val: &str,
ns: Option<Namespace>, ns: Option<Namespace>,
tag: Option<&[u8]>, tag: Option<&str>,
namespaces: &HashMap<Namespace, &[u8]>, namespaces: &HashMap<Namespace, &str>,
writer: &mut Writer<&mut Vec<u8>>, writer: &mut Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
val.to_uppercase().serialize(ns, tag, namespaces, writer) val.to_uppercase().serialize(ns, tag, namespaces, writer)
@@ -160,7 +160,7 @@ fn test_struct_serialize_with() {
#[test] #[test]
fn test_struct_tag_list() { fn test_struct_tag_list() {
#[derive(Debug, XmlRootTag, XmlSerialize, XmlDeserialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, XmlDeserialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ty = "untagged", flatten)] #[xml(ty = "untagged", flatten)]
tags: Vec<Tag>, tags: Vec<Tag>,
@@ -194,9 +194,9 @@ fn test_struct_ns() {
const NS: Namespace = quick_xml::name::Namespace(b"NS:TEST:"); const NS: Namespace = quick_xml::name::Namespace(b"NS:TEST:");
#[derive(Debug, XmlRootTag, XmlSerialize)] #[derive(Debug, XmlRootTag, XmlSerialize)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
#[xml(ns = "NS", rename = b"okay")] #[xml(ns = "NS", rename = "okay")]
child: String, child: String,
} }
@@ -210,7 +210,7 @@ fn test_struct_ns() {
#[test] #[test]
fn test_struct_tuple() { fn test_struct_tuple() {
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)] #[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document { struct Document {
child: Child, child: Child,
} }
@@ -230,8 +230,8 @@ fn test_tuple_struct() {
const NS: Namespace = quick_xml::name::Namespace(b"NS:TEST:"); const NS: Namespace = quick_xml::name::Namespace(b"NS:TEST:");
#[derive(Debug, XmlRootTag, XmlSerialize)] #[derive(Debug, XmlRootTag, XmlSerialize)]
#[xml(root = b"document")] #[xml(root = "document")]
struct Document(#[xml(ns = "NS", rename = b"okay")] String); struct Document(#[xml(ns = "NS", rename = "okay")] String);
Document("hello!".to_string()) Document("hello!".to_string())
.serialize_to_string() .serialize_to_string()

View File

@@ -3,10 +3,10 @@
a CalDAV/CardDAV server a CalDAV/CardDAV server
!!! warning !!! warning
RustiCal is under **active development**! RustiCal is under **active development**!
While I've been successfully using RustiCal productively for a few weeks now, While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
you'd still be one of the first testers so expect bugs and rough edges. you'd still be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
[Installation](installation/index.md){ .md-button } [Installation](installation/index.md){ .md-button }
@@ -14,6 +14,7 @@ a CalDAV/CardDAV server
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- also export feature in the frontend - also export feature in the frontend
- Import your existing calendars in the frontend
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5 - **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)

View File

@@ -38,11 +38,11 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
oidc_config: Option<OidcConfig>, oidc_config: Option<OidcConfig>,
nextcloud_login_config: NextcloudLoginConfig, nextcloud_login_config: NextcloudLoginConfig,
dav_push_enabled: bool, dav_push_enabled: bool,
session_cookie_samesite_strict: bool,
) -> Router<()> { ) -> Router<()> {
let combined_cal_store = Arc::new(CombinedCalendarStore::new( let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
cal_store.clone(), let combined_cal_store =
ContactBirthdayStore::new(addr_store.clone()).into(), Arc::new(CombinedCalendarStore::new(cal_store.clone()).with_store(birthday_store));
));
let mut router = Router::new() let mut router = Router::new()
.merge(caldav_router( .merge(caldav_router(
@@ -128,7 +128,11 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
SessionManagerLayer::new(session_store) SessionManagerLayer::new(session_store)
.with_name("rustical_session") .with_name("rustical_session")
.with_secure(true) .with_secure(true)
.with_same_site(SameSite::Strict) .with_same_site(if session_cookie_samesite_strict {
SameSite::Strict
} else {
SameSite::Lax
})
.with_expiry(Expiry::OnInactivity( .with_expiry(Expiry::OnInactivity(
tower_sessions::cookie::time::Duration::hours(2), tower_sessions::cookie::time::Duration::hours(2),
)), )),

View File

@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub struct HttpConfig { pub struct HttpConfig {
pub host: String, pub host: String,
pub port: u16, pub port: u16,
pub session_cookie_samesite_strict: bool,
} }
impl Default for HttpConfig { impl Default for HttpConfig {
@@ -14,6 +15,7 @@ impl Default for HttpConfig {
Self { Self {
host: "0.0.0.0".to_owned(), host: "0.0.0.0".to_owned(),
port: 4000, port: 4000,
session_cookie_samesite_strict: false,
} }
} }
} }

View File

@@ -116,6 +116,7 @@ async fn main() -> Result<()> {
config.oidc.clone(), config.oidc.clone(),
config.nextcloud_login.clone(), config.nextcloud_login.clone(),
config.dav_push.enabled, config.dav_push.enabled,
config.http.session_cookie_samesite_strict,
); );
let app = ServiceExt::<Request>::into_make_service( let app = ServiceExt::<Request>::into_make_service(
NormalizePathLayer::trim_trailing_slash().layer(app), NormalizePathLayer::trim_trailing_slash().layer(app),