Work on axum support

This commit is contained in:
Lennart
2025-06-07 20:17:50 +02:00
parent 57832116aa
commit 790c657b08
38 changed files with 582 additions and 64 deletions

91
Cargo.lock generated
View File

@@ -489,6 +489,83 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.3.1",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"futures-util",
"headers",
"http 1.3.1",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.75" version = "0.3.75"
@@ -1565,6 +1642,7 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@@ -1974,6 +2052,12 @@ dependencies = [
"regex-automata 0.1.10", "regex-automata 0.1.10",
] ]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@@ -3076,6 +3160,7 @@ dependencies = [
"chrono-tz", "chrono-tz",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures-util", "futures-util",
"http 1.3.1",
"quick-xml", "quick-xml",
"rustical_dav", "rustical_dav",
"rustical_dav_push", "rustical_dav_push",
@@ -3103,6 +3188,7 @@ dependencies = [
"chrono", "chrono",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures-util", "futures-util",
"http 1.3.1",
"quick-xml", "quick-xml",
"rustical_dav", "rustical_dav",
"rustical_dav_push", "rustical_dav_push",
@@ -3124,10 +3210,13 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"async-trait", "async-trait",
"axum",
"axum-extra",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures-util", "futures-util",
"headers", "headers",
"http 0.2.12", "http 0.2.12",
"http 1.3.1",
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"quick-xml", "quick-xml",
@@ -3135,6 +3224,7 @@ dependencies = [
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tower",
"tracing", "tracing",
"tracing-actix-web", "tracing-actix-web",
] ]
@@ -3147,6 +3237,7 @@ dependencies = [
"async-trait", "async-trait",
"derive_more 2.0.1", "derive_more 2.0.1",
"futures-util", "futures-util",
"http 1.3.1",
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"quick-xml", "quick-xml",

View File

@@ -86,7 +86,8 @@ sqlx = { version = "0.8", default-features = false, features = [
"migrate", "migrate",
"json", "json",
] } ] }
http = "0.2" # This version is used by actix-web http_02 = { package = "http", version = "0.2" } # actix-web uses a very outdated version
http = "1.3"
headers = "0.4" headers = "0.4"
strum = "0.27" strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"

View File

@@ -29,3 +29,4 @@ rustical_xml.workspace = true
uuid.workspace = true uuid.workspace = true
rustical_dav_push.workspace = true rustical_dav_push.workspace = true
rustical_ical.workspace = true rustical_ical.workspace = true
http.workspace = true

View File

@@ -6,11 +6,11 @@ use crate::{
}; };
use actix_web::{ use actix_web::{
HttpRequest, Responder, HttpRequest, Responder,
http::StatusCode,
web::{Data, Path}, web::{Data, Path},
}; };
use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget}; use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query}; use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::{PrincipalUri, Resource}, resource::{PrincipalUri, Resource},
xml::{ xml::{

View File

@@ -4,7 +4,7 @@ use crate::{
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource, CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource,
}, },
}; };
use actix_web::http::StatusCode; use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::{PrincipalUri, Resource}, resource::{PrincipalUri, Resource},
xml::{ xml::{

View File

@@ -322,7 +322,7 @@ impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> { impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
type MemberType = CalendarObjectResource; type MemberType = CalendarObjectResource;
type PathComponents = (String, String); // principal, calendar_id type PathComponents = (String, String); // principal, calendar_id
@@ -331,6 +331,8 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
type Principal = User; type Principal = User;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
async fn get_resource( async fn get_resource(
&self, &self,
(principal, cal_id): &Self::PathComponents, (principal, cal_id): &Self::PathComponents,

View File

@@ -137,7 +137,7 @@ pub struct CalendarObjectPathComponents {
pub object_id: String, pub object_id: String,
} }
#[async_trait(?Send)] #[async_trait]
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> { impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type PathComponents = CalendarObjectPathComponents; type PathComponents = CalendarObjectPathComponents;
type Resource = CalendarObjectResource; type Resource = CalendarObjectResource;
@@ -146,6 +146,8 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type Principal = User; type Principal = User;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
async fn get_resource( async fn get_resource(
&self, &self,
CalendarObjectPathComponents { CalendarObjectPathComponents {

View File

@@ -77,7 +77,7 @@ impl<C: CalendarStore, S: SubscriptionStore> CalendarSetResourceService<C, S> {
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetResourceService<C, S> { impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetResourceService<C, S> {
type PathComponents = (String,); type PathComponents = (String,);
type MemberType = CalendarResource; type MemberType = CalendarResource;
@@ -86,6 +86,8 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetReso
type Principal = User; type Principal = User;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol";
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,

View File

@@ -71,7 +71,8 @@ impl actix_web::ResponseError for Error {
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::DavError(err) => err.status_code(), Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
.expect("Just converting between versions"),
Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::Unauthorized => StatusCode::UNAUTHORIZED,
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,

View File

@@ -145,7 +145,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore> impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
ResourceService for PrincipalResourceService<AP, S, CS, BS> ResourceService for PrincipalResourceService<AP, S, CS, BS>
{ {
@@ -156,6 +156,8 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
type Principal = User; type Principal = User;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control";
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,

View File

@@ -27,3 +27,4 @@ rustical_xml.workspace = true
uuid.workspace = true uuid.workspace = true
rustical_dav_push.workspace = true rustical_dav_push.workspace = true
rustical_ical.workspace = true rustical_ical.workspace = true
http.workspace = true

View File

@@ -105,7 +105,7 @@ pub struct AddressObjectPathComponents {
pub object_id: String, pub object_id: String,
} }
#[async_trait(?Send)] #[async_trait]
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> { impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
type PathComponents = AddressObjectPathComponents; type PathComponents = AddressObjectPathComponents;
type Resource = AddressObjectResource; type Resource = AddressObjectResource;
@@ -114,6 +114,8 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
type Principal = User; type Principal = User;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
async fn get_resource( async fn get_resource(
&self, &self,
AddressObjectPathComponents { AddressObjectPathComponents {

View File

@@ -4,10 +4,9 @@ use crate::{
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource, AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
}, },
}; };
use actix_web::{ use actix_web::dev::{Path, ResourceDef};
dev::{Path, ResourceDef},
http::StatusCode, use http::StatusCode;
};
use rustical_dav::{ use rustical_dav::{
resource::{PrincipalUri, Resource}, resource::{PrincipalUri, Resource},
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement}, xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},

View File

@@ -4,7 +4,7 @@ use crate::{
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource, AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
}, },
}; };
use actix_web::http::StatusCode; use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::{PrincipalUri, Resource}, resource::{PrincipalUri, Resource},
xml::{ xml::{

View File

@@ -188,7 +188,7 @@ impl Resource for AddressbookResource {
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
for AddressbookResourceService<AS, S> for AddressbookResourceService<AS, S>
{ {
@@ -199,6 +199,8 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
type Principal = User; type Principal = User;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
async fn get_resource( async fn get_resource(
&self, &self,
(principal, addressbook_id): &Self::PathComponents, (principal, addressbook_id): &Self::PathComponents,

View File

@@ -38,7 +38,8 @@ impl actix_web::ResponseError for Error {
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::DavError(err) => err.status_code(), Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
.expect("Just converting between versions"),
Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::Unauthorized => StatusCode::UNAUTHORIZED,
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,

View File

@@ -140,7 +140,7 @@ impl Resource for PrincipalResource {
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> ResourceService impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> ResourceService
for PrincipalResourceService<A, AP, S> for PrincipalResourceService<A, AP, S>
{ {
@@ -151,6 +151,8 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
type Principal = User; type Principal = User;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
async fn get_resource( async fn get_resource(
&self, &self,
(principal,): &Self::PathComponents, (principal,): &Self::PathComponents,

View File

@@ -7,9 +7,16 @@ repository.workspace = true
publish = false publish = false
[features] [features]
actix = ["dep:actix-web", "dep:tracing-actix-web"] actix = ["dep:actix-web", "dep:tracing-actix-web", "dep:http_02"]
axum = ["dep:axum", "dep:axum-extra", "dep:tower"]
[dependencies] [dependencies]
axum = { version = "0.8", optional = true }
axum-extra = { version = "0.10", optional = true, features = ["typed-header"] }
tower = { version = "0.5", optional = true }
http_02 = { workspace = true, optional = true }
rustical_xml.workspace = true rustical_xml.workspace = true
async-trait.workspace = true async-trait.workspace = true
futures-util.workspace = true futures-util.workspace = true

View File

@@ -55,17 +55,40 @@ impl Error {
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
impl actix_web::error::ResponseError for Error { impl actix_web::error::ResponseError for Error {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> actix_web::http::StatusCode {
self.status_code() self.status_code()
.as_u16()
.try_into()
.expect("Just converting between versions")
} }
fn error_response(&self) -> actix_web::HttpResponse { fn error_response(&self) -> actix_web::HttpResponse {
use actix_web::ResponseError;
error!("Error: {self}"); error!("Error: {self}");
match self { match self {
Error::Unauthorized => actix_web::HttpResponse::build(self.status_code()) Error::Unauthorized => actix_web::HttpResponse::build(ResponseError::status_code(self))
.append_header(("WWW-Authenticate", "Basic")) .append_header(("WWW-Authenticate", "Basic"))
.body(self.to_string()), .body(self.to_string()),
_ => actix_web::HttpResponse::build(self.status_code()).body(self.to_string()), _ => actix_web::HttpResponse::build(ResponseError::status_code(self))
.body(self.to_string()),
} }
} }
} }
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
use axum::body::Body;
let mut resp = axum::response::Response::builder().status(self.status_code());
if matches!(&self, &Error::Unauthorized) {
resp.headers_mut()
.expect("This must always work")
.insert("WWW-Authenticate", "Basic".parse().unwrap());
}
resp.body(Body::new(self.to_string()))
.expect("This should always work")
}
}

View File

@@ -1,7 +1,8 @@
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
use actix_web::{HttpRequest, ResponseError}; use actix_web::{HttpRequest, ResponseError};
#[cfg(feature = "axum")]
use axum::{body::Body, extract::FromRequestParts, response::IntoResponse};
use futures_util::future::{Ready, err, ok}; use futures_util::future::{Ready, err, ok};
use http::StatusCode;
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError}; use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError};
use thiserror::Error; use thiserror::Error;
@@ -12,7 +13,17 @@ pub struct InvalidDepthHeader;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
impl ResponseError for InvalidDepthHeader { impl ResponseError for InvalidDepthHeader {
fn status_code(&self) -> actix_web::http::StatusCode { fn status_code(&self) -> actix_web::http::StatusCode {
StatusCode::BAD_REQUEST http_02::StatusCode::BAD_REQUEST
}
}
#[cfg(feature = "axum")]
impl IntoResponse for InvalidDepthHeader {
fn into_response(self) -> axum::response::Response {
axum::response::Response::builder()
.status(axum::http::StatusCode::BAD_REQUEST)
.body(Body::empty())
.expect("this always works")
} }
} }
@@ -81,3 +92,19 @@ impl actix_web::FromRequest for Depth {
Self::extract(req) Self::extract(req)
} }
} }
#[cfg(feature = "axum")]
impl<S: Send + Sync> FromRequestParts<S> for Depth {
type Rejection = InvalidDepthHeader;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
if let Some(depth_header) = parts.headers.get("Depth") {
depth_header.as_bytes().try_into()
} else {
Ok(Self::Zero)
}
}
}

View File

@@ -9,6 +9,6 @@ pub mod xml;
pub use error::Error; pub use error::Error;
pub trait Principal: std::fmt::Debug + Clone + 'static { pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static {
fn get_id(&self) -> &str; fn get_id(&self) -> &str;
} }

View File

@@ -0,0 +1,94 @@
use axum::body::Body;
use futures_util::future::BoxFuture;
use headers::Allow;
use http::{Method, Request, Response};
use std::{convert::Infallible, str::FromStr, sync::Arc};
pub type MethodFunction<Service> =
fn(Arc<Service>, Request<Body>) -> BoxFuture<'static, Result<Response<Body>, Infallible>>;
pub trait AxumMethods: Sized + Send + Sync + 'static {
#[inline]
fn report() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn get() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn head() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn post() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn mkcol() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn mkcalendar() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn copy() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn mv() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn put() -> Option<MethodFunction<Self>> {
None
}
#[inline]
fn allow_header() -> Allow {
let mut allow = vec![
Method::from_str("PROPFIND").unwrap(),
Method::from_str("PROPPATCH").unwrap(),
Method::DELETE,
Method::OPTIONS,
];
if Self::report().is_some() {
allow.push(Method::from_str("REPORT").unwrap());
}
if Self::get().is_some() {
allow.push(Method::GET);
}
if Self::head().is_some() {
allow.push(Method::HEAD);
}
if Self::post().is_some() {
allow.push(Method::POST);
}
if Self::mkcol().is_some() {
allow.push(Method::from_str("MKCOL").unwrap());
}
if Self::mkcalendar().is_some() {
allow.push(Method::from_str("MKCALENDAR").unwrap());
}
if Self::copy().is_some() {
allow.push(Method::from_str("COPY").unwrap());
}
if Self::mv().is_some() {
allow.push(Method::from_str("MOVE").unwrap());
}
if Self::put().is_some() {
allow.push(Method::PUT);
}
allow.into_iter().collect()
}
}

View File

@@ -0,0 +1,120 @@
use super::methods::{axum_route_propfind, axum_route_proppatch};
use crate::resource::{ResourceService, axum_methods::AxumMethods};
use axum::{
body::Body,
handler::Handler,
http::{Request, Response},
response::IntoResponse,
};
use futures_util::future::BoxFuture;
use headers::HeaderMapExt;
use http::{HeaderValue, StatusCode};
use std::{convert::Infallible, sync::Arc};
use tower::Service;
#[derive(Clone)]
pub struct AxumService<RS: ResourceService + AxumMethods> {
resource_service: Arc<RS>,
}
impl<RS: ResourceService + AxumMethods> AxumService<RS> {
pub fn new(resource_service: Arc<RS>) -> Self {
Self { resource_service }
}
}
impl<RS: ResourceService + AxumMethods + Clone + Send + Sync> Service<Request<Body>>
for AxumService<RS>
where
RS::Error: IntoResponse + Send + Sync + 'static,
{
type Error = Infallible;
type Response = Response<Body>;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
#[inline]
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
Ok(()).into()
}
#[inline]
fn call(&mut self, req: Request<Body>) -> Self::Future {
use crate::resource::methods::axum_route_delete;
let mut propfind_service =
Handler::with_state(axum_route_propfind::<RS>, self.resource_service.clone());
let mut proppatch_service =
Handler::with_state(axum_route_proppatch::<RS>, self.resource_service.clone());
let mut delete_service =
Handler::with_state(axum_route_delete::<RS>, self.resource_service.clone());
let mut options_service = Handler::with_state(route_options::<RS>, ());
match req.method().as_str() {
"PROPFIND" => return Box::pin(Service::call(&mut propfind_service, req)),
"PROPPATCH" => return Box::pin(Service::call(&mut proppatch_service, req)),
"DELETE" => return Box::pin(Service::call(&mut delete_service, req)),
"OPTIONS" => return Box::pin(Service::call(&mut options_service, req)),
"REPORT" => {
if let Some(svc) = RS::report() {
return svc(self.resource_service.clone(), req);
}
}
"GET" => {
if let Some(svc) = RS::get() {
return svc(self.resource_service.clone(), req);
}
}
"HEAD" => {
if let Some(svc) = RS::head() {
return svc(self.resource_service.clone(), req);
}
}
"POST" => {
if let Some(svc) = RS::post() {
return svc(self.resource_service.clone(), req);
}
}
"MKCOL" => {
if let Some(svc) = RS::mkcol() {
return svc(self.resource_service.clone(), req);
}
}
"MKCALENDAR" => {
if let Some(svc) = RS::mkcalendar() {
return svc(self.resource_service.clone(), req);
}
}
"COPY" => {
if let Some(svc) = RS::copy() {
return svc(self.resource_service.clone(), req);
}
}
"MOVE" => {
if let Some(svc) = RS::mv() {
return svc(self.resource_service.clone(), req);
}
}
"PUT" => {
if let Some(svc) = RS::put() {
return svc(self.resource_service.clone(), req);
}
}
_ => {}
};
Box::pin(async move {
Ok(Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.body(Body::from("Method not allowed"))
.unwrap())
})
}
}
async fn route_options<RS: ResourceService + AxumMethods>() -> Response<Body> {
let mut resp = Response::builder().status(StatusCode::OK);
let headers = resp.headers_mut().unwrap();
headers.insert("DAV", HeaderValue::from_static(RS::DAV_HEADER));
headers.typed_insert(RS::allow_header());
resp.body(Body::empty()).unwrap()
}

View File

@@ -2,9 +2,17 @@ use crate::Error;
use crate::privileges::UserPrivilege; use crate::privileges::UserPrivilege;
use crate::resource::Resource; use crate::resource::Resource;
use crate::resource::ResourceService; use crate::resource::ResourceService;
#[cfg(feature = "axum")]
use axum::extract::{Extension, Path, State};
#[cfg(feature = "axum")]
use axum_extra::TypedHeader;
use headers::Header; use headers::Header;
use headers::{HeaderValue, IfMatch, IfNoneMatch}; use headers::{HeaderValue, IfMatch, IfNoneMatch};
#[cfg(feature = "axum")]
use http::HeaderMap;
use itertools::Itertools; use itertools::Itertools;
#[cfg(feature = "axum")]
use std::sync::Arc;
use tracing::instrument; use tracing::instrument;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
@@ -27,12 +35,12 @@ pub async fn actix_route_delete<R: ResourceService>(
// while actix-web still uses http==0.2 // while actix-web still uses http==0.2
let if_match = req let if_match = req
.headers() .headers()
.get_all(http::header::IF_MATCH) .get_all(http_02::header::IF_MATCH)
.map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap())
.collect_vec(); .collect_vec();
let if_none_match = req let if_none_match = req
.headers() .headers()
.get_all(http::header::IF_NONE_MATCH) .get_all(http_02::header::IF_NONE_MATCH)
.map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap())
.collect_vec(); .collect_vec();
@@ -49,7 +57,7 @@ pub async fn actix_route_delete<R: ResourceService>(
route_delete( route_delete(
&path.into_inner(), &path.into_inner(),
principal, &principal,
resource_service.as_ref(), resource_service.as_ref(),
no_trash, no_trash,
if_match, if_match,
@@ -60,9 +68,33 @@ pub async fn actix_route_delete<R: ResourceService>(
Ok(actix_web::HttpResponse::Ok().body("")) Ok(actix_web::HttpResponse::Ok().body(""))
} }
#[cfg(feature = "axum")]
pub(crate) async fn axum_route_delete<R: ResourceService>(
Path(path): Path<R::PathComponents>,
State(resource_service): State<Arc<R>>,
Extension(principal): Extension<R::Principal>,
if_match: Option<TypedHeader<IfMatch>>,
if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap,
) -> Result<(), R::Error> {
let no_trash = header_map
.get("X-No-Trashbin")
.map(|val| matches!(val.to_str(), Ok("1")))
.unwrap_or(false);
route_delete(
&path,
&principal,
resource_service.as_ref(),
no_trash,
if_match.map(|hdr| hdr.0),
if_none_match.map(|hdr| hdr.0),
)
.await
}
pub async fn route_delete<R: ResourceService>( pub async fn route_delete<R: ResourceService>(
path_components: &R::PathComponents, path_components: &R::PathComponents,
principal: R::Principal, principal: &R::Principal,
resource_service: &R, resource_service: &R,
no_trash: bool, no_trash: bool,
if_match: Option<IfMatch>, if_match: Option<IfMatch>,
@@ -70,7 +102,7 @@ pub async fn route_delete<R: ResourceService>(
) -> Result<(), R::Error> { ) -> Result<(), R::Error> {
let resource = resource_service.get_resource(path_components).await?; let resource = resource_service.get_resource(path_components).await?;
let privileges = resource.get_user_privileges(&principal)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Write) { if !privileges.has(&UserPrivilege::Write) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());
} }

View File

@@ -2,15 +2,17 @@ mod delete;
mod propfind; mod propfind;
mod proppatch; mod proppatch;
pub(crate) use delete::route_delete;
pub(crate) use propfind::route_propfind;
pub(crate) use proppatch::route_proppatch;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
pub(crate) use delete::actix_route_delete; pub(crate) use delete::actix_route_delete;
#[cfg(feature = "axum")]
pub(crate) use delete::axum_route_delete;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
pub(crate) use propfind::actix_route_propfind; pub(crate) use propfind::actix_route_propfind;
#[cfg(feature = "axum")]
pub(crate) use propfind::axum_route_propfind;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
pub(crate) use proppatch::actix_route_proppatch; pub(crate) use proppatch::actix_route_proppatch;
#[cfg(feature = "axum")]
pub(crate) use proppatch::axum_route_proppatch;

View File

@@ -7,8 +7,11 @@ use crate::resource::ResourceService;
use crate::xml::MultistatusElement; use crate::xml::MultistatusElement;
use crate::xml::PropfindElement; use crate::xml::PropfindElement;
use crate::xml::PropfindType; use crate::xml::PropfindType;
#[cfg(feature = "axum")]
use axum::extract::{Extension, OriginalUri, Path, State};
use rustical_xml::PropName; use rustical_xml::PropName;
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use std::sync::Arc;
use tracing::instrument; use tracing::instrument;
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
@@ -30,21 +33,46 @@ pub(crate) async fn actix_route_propfind<R: ResourceService>(
route_propfind( route_propfind(
&path.into_inner(), &path.into_inner(),
req.path(), req.path(),
body, &body,
user, &user,
depth, &depth,
resource_service.as_ref(), resource_service.as_ref(),
puri.as_ref(), puri.as_ref(),
) )
.await .await
} }
#[cfg(feature = "axum")]
pub(crate) async fn axum_route_propfind<R: ResourceService>(
Path(path): Path<R::PathComponents>,
State(resource_service): State<Arc<R>>,
depth: Depth,
Extension(principal): Extension<R::Principal>,
uri: OriginalUri,
Extension(puri): Extension<R::PrincipalUri>,
body: String,
) -> Result<
MultistatusElement<<R::Resource as Resource>::Prop, <R::MemberType as Resource>::Prop>,
R::Error,
> {
route_propfind::<R>(
&path,
uri.path(),
&body,
&principal,
&depth,
resource_service.as_ref(),
&puri,
)
.await
}
pub(crate) async fn route_propfind<R: ResourceService>( pub(crate) async fn route_propfind<R: ResourceService>(
path_components: &R::PathComponents, path_components: &R::PathComponents,
path: &str, path: &str,
body: String, body: &str,
user: R::Principal, principal: &R::Principal,
depth: Depth, depth: &Depth,
resource_service: &R, resource_service: &R,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
) -> Result< ) -> Result<
@@ -52,7 +80,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
R::Error, R::Error,
> { > {
let resource = resource_service.get_resource(path_components).await?; let resource = resource_service.get_resource(path_components).await?;
let privileges = resource.get_user_privileges(&user)?; let privileges = resource.get_user_privileges(principal)?;
if !privileges.has(&UserPrivilege::Read) { if !privileges.has(&UserPrivilege::Read) {
return Err(Error::Unauthorized.into()); return Err(Error::Unauthorized.into());
} }
@@ -60,7 +88,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
// A request body is optional. If empty we MUST return all props // A request body is optional. If empty we MUST return all props
let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> = let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> =
if !body.is_empty() { if !body.is_empty() {
PropfindElement::parse_str(&body).map_err(Error::XmlError)? PropfindElement::parse_str(body).map_err(Error::XmlError)?
} else { } else {
PropfindElement { PropfindElement {
prop: PropfindType::Allprop, prop: PropfindType::Allprop,
@@ -68,7 +96,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
}; };
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> = let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
if !body.is_empty() { if !body.is_empty() {
PropfindElement::parse_str(&body).map_err(Error::XmlError)? PropfindElement::parse_str(body).map_err(Error::XmlError)?
} else { } else {
PropfindElement { PropfindElement {
prop: PropfindType::Allprop, prop: PropfindType::Allprop,
@@ -76,18 +104,18 @@ pub(crate) async fn route_propfind<R: ResourceService>(
}; };
let mut member_responses = Vec::new(); let mut member_responses = Vec::new();
if depth != Depth::Zero { if depth != &Depth::Zero {
for (subpath, member) in resource_service.get_members(path_components).await? { for (subpath, member) in resource_service.get_members(path_components).await? {
member_responses.push(member.propfind_typed( member_responses.push(member.propfind_typed(
&format!("{}/{}", path.trim_end_matches('/'), subpath), &format!("{}/{}", path.trim_end_matches('/'), subpath),
&propfind_member.prop, &propfind_member.prop,
puri, puri,
&user, principal,
)?); )?);
} }
} }
let response = resource.propfind_typed(path, &propfind_self.prop, puri, &user)?; let response = resource.propfind_typed(path, &propfind_self.prop, puri, &principal)?;
Ok(MultistatusElement { Ok(MultistatusElement {
responses: vec![response], responses: vec![response],

View File

@@ -1,7 +1,10 @@
use crate::Error; use crate::Error;
use crate::privileges::UserPrivilege; use crate::privileges::UserPrivilege;
use std::sync::Arc;
use crate::resource::Resource; use crate::resource::Resource;
use crate::resource::ResourceService; use crate::resource::ResourceService;
#[cfg(feature = "axum")]
use axum::extract::{Extension, OriginalUri, Path, State};
use crate::xml::MultistatusElement; use crate::xml::MultistatusElement;
use crate::xml::TagList; use crate::xml::TagList;
use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement}; use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement};
@@ -74,8 +77,30 @@ pub(crate) async fn actix_route_proppatch<R: ResourceService>(
route_proppatch( route_proppatch(
&path.into_inner(), &path.into_inner(),
req.path(), req.path(),
body, &body,
principal, &principal,
resource_service.as_ref(),
)
.await
}
#[cfg(feature = "axum")]
pub(crate) async fn axum_route_proppatch<R: ResourceService>(
Path(path): Path<R::PathComponents>,
State(resource_service): State<Arc<R>>,
Extension(principal): Extension<R::Principal>,
uri: OriginalUri,
body: String,
) -> Result<
MultistatusElement<String, String>,
R::Error,
> {
route_proppatch(
&path,
uri.path(),
&body,
&principal,
resource_service.as_ref(), resource_service.as_ref(),
) )
.await .await
@@ -84,8 +109,8 @@ pub(crate) async fn actix_route_proppatch<R: ResourceService>(
pub(crate) async fn route_proppatch<R: ResourceService>( pub(crate) async fn route_proppatch<R: ResourceService>(
path_components: &R::PathComponents, path_components: &R::PathComponents,
path: &str, path: &str,
body: String, body: &str,
principal: R::Principal, principal: &R::Principal,
resource_service: &R, resource_service: &R,
) -> Result<MultistatusElement<String, String>, R::Error> { ) -> Result<MultistatusElement<String, String>, R::Error> {
let href = path.to_owned(); let href = path.to_owned();

View File

@@ -12,12 +12,19 @@ use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSe
use std::collections::HashSet; use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
#[cfg(feature = "axum")]
mod axum_methods;
#[cfg(feature = "axum")]
mod axum_service;
mod methods; mod methods;
mod principal_uri; mod principal_uri;
mod resource_service; mod resource_service;
#[cfg(feature = "axum")]
pub use axum_methods::AxumMethods;
#[cfg(feature = "axum")]
pub use axum_service::AxumService;
pub use principal_uri::PrincipalUri; pub use principal_uri::PrincipalUri;
pub use resource_service::*;
pub trait ResourceProp: XmlSerialize + XmlDeserialize {} pub trait ResourceProp: XmlSerialize + XmlDeserialize {}
impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {} impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
@@ -25,8 +32,8 @@ impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
pub trait ResourcePropName: FromStr {} pub trait ResourcePropName: FromStr {}
impl<T: FromStr> ResourcePropName for T {} impl<T: FromStr> ResourcePropName for T {}
pub trait Resource: Clone + 'static { pub trait Resource: Clone + Send + 'static {
type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName; type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName + Send;
type Error: From<crate::Error>; type Error: From<crate::Error>;
type Principal: Principal; type Principal: Principal;

View File

@@ -1,3 +1,3 @@
pub trait PrincipalUri: 'static { pub trait PrincipalUri: 'static + Clone + Send + Sync {
fn principal_uri(&self, principal: &str) -> String; fn principal_uri(&self, principal: &str) -> String;
} }

View File

@@ -2,28 +2,37 @@
use super::methods::{actix_route_delete, actix_route_propfind, actix_route_proppatch}; use super::methods::{actix_route_delete, actix_route_propfind, actix_route_proppatch};
use super::{PrincipalUri, Resource}; use super::{PrincipalUri, Resource};
use crate::Principal; use crate::Principal;
#[cfg(feature = "axum")]
use crate::resource::{AxumMethods, AxumService};
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
use actix_web::{http::Method, web, web::Data}; use actix_web::{http::Method, web, web::Data};
use async_trait::async_trait; use async_trait::async_trait;
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::{str::FromStr, sync::Arc};
#[async_trait(?Send)] #[async_trait]
pub trait ResourceService: Sized + 'static { pub trait ResourceService: Sized + Send + Sync + 'static {
type PathComponents: for<'de> Deserialize<'de> + Sized + Send + Sync + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>; type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>;
type PathComponents: for<'de> Deserialize<'de> + Sized + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>; type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
type Error: From<crate::Error>; type Error: From<crate::Error> + Send;
type Principal: Principal; type Principal: Principal;
type PrincipalUri: PrincipalUri; type PrincipalUri: PrincipalUri;
const DAV_HEADER: &'static str;
async fn get_members( async fn get_members(
&self, &self,
_path_components: &Self::PathComponents, _path: &Self::PathComponents,
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> { ) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
Ok(vec![]) Ok(vec![])
} }
async fn test_get_members(&self, _path: &Self::PathComponents) -> Result<String, Self::Error> {
// ) -> Result<Vec<Self::MemberType>, Self::Error> {
Ok("asd".to_string())
}
async fn get_resource( async fn get_resource(
&self, &self,
_path: &Self::PathComponents, _path: &Self::PathComponents,
@@ -69,4 +78,12 @@ pub trait ResourceService: Sized + 'static {
where where
Self::Error: actix_web::ResponseError, Self::Error: actix_web::ResponseError,
Self::Principal: actix_web::FromRequest; Self::Principal: actix_web::FromRequest;
#[cfg(feature = "axum")]
fn axum_service(self) -> AxumService<Self>
where
Self: Clone + Send + Sync + AxumMethods,
{
AxumService::new(Arc::new(self))
}
} }

View File

@@ -58,7 +58,7 @@ impl<PRS: ResourceService + Clone, P: Principal, PURI: PrincipalUri>
} }
} }
#[async_trait(?Send)] #[async_trait]
impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalUri> ResourceService impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalUri> ResourceService
for RootResourceService<PRS, P, PURI> for RootResourceService<PRS, P, PURI>
{ {
@@ -69,6 +69,8 @@ impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalU
type Principal = P; type Principal = P;
type PrincipalUri = PURI; type PrincipalUri = PURI;
const DAV_HEADER: &str = "1, 3, access-control";
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> { async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
Ok(RootResource::<PRS::Resource, P>::default()) Ok(RootResource::<PRS::Resource, P>::default())
} }

View File

@@ -124,3 +124,25 @@ impl<T1: XmlSerialize, T2: XmlSerialize> Responder for MultistatusElement<T1, T2
.body(String::from_utf8(output).unwrap()) .body(String::from_utf8(output).unwrap())
} }
} }
#[cfg(feature = "axum")]
impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
for MultistatusElement<T1, T2>
{
fn into_response(self) -> axum::response::Response {
use axum::body::Body;
use http::header;
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
if let Err(err) = self.serialize_root(&mut writer) {
return crate::Error::from(err).into_response();
}
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
resp.headers_mut()
.unwrap()
.insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap());
resp.body(Body::from(output)).unwrap()
}
}

View File

@@ -23,3 +23,4 @@ reqwest.workspace = true
tokio.workspace = true tokio.workspace = true
rustical_dav.workspace = true rustical_dav.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
http.workspace = true

View File

@@ -1,4 +1,4 @@
use actix_web::http::StatusCode; use http::StatusCode;
use reqwest::{ use reqwest::{
Method, Request, Method, Request,
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},

View File

@@ -335,7 +335,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>); struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
#[async_trait(?Send)] #[async_trait]
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> { impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
type Error = rustical_store::Error; type Error = rustical_store::Error;

View File

@@ -1,7 +1,7 @@
use actix_web::ResponseError; use actix_web::ResponseError;
use async_trait::async_trait; use async_trait::async_trait;
#[async_trait(?Send)] #[async_trait]
pub trait UserStore: 'static { pub trait UserStore: 'static {
type Error: ResponseError; type Error: ResponseError;

View File

@@ -4,7 +4,7 @@ use crate::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
#[async_trait] #[async_trait]
pub trait AuthenticationProvider: 'static { pub trait AuthenticationProvider: Send + Sync + 'static {
async fn get_principals(&self) -> Result<Vec<User>, crate::Error>; async fn get_principals(&self) -> Result<Vec<User>, crate::Error>;
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>; async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>; async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>;

View File

@@ -41,6 +41,8 @@ pub trait EnumVariants {
pub trait PropName: Sized { pub trait PropName: Sized {
type Names: Into<(Option<Namespace<'static>>, &'static str)> type Names: Into<(Option<Namespace<'static>>, &'static str)>
+ Clone + Clone
+ Send
+ Sync
+ From<Self> + From<Self>
+ FromStr<Err: std::fmt::Debug> + FromStr<Err: std::fmt::Debug>
+ Hash + Hash