provide basic responder functionality without serde, json features with serde feature

This commit is contained in:
Paul Zinselmeyer 2023-10-22 20:45:48 +02:00
parent a32274e280
commit ad16b10fc6
Signed by: pfzetto
GPG key ID: 4EEF46A5B276E648
4 changed files with 252 additions and 137 deletions

View file

@ -11,9 +11,9 @@ version = "0.3.1"
edition = "2021" edition = "2021"
[features] [features]
default = [ "responders" ] default = [ ]
guards = ["tower", "futures-core", "pin-project-lite"] guards = ["tower", "futures-core", "pin-project-lite"]
responders = ["serde", "serde_json"] serde = [ "dep:serde", "dep:serde_json" ]
[dependencies] [dependencies]
axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false } axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false }

View file

@ -4,7 +4,6 @@ pub mod extractors;
#[cfg(feature = "guards")] #[cfg(feature = "guards")]
pub mod guard; pub mod guard;
pub mod headers; pub mod headers;
#[cfg(feature = "responders")]
pub mod responders; pub mod responders;
use axum::{ use axum::{
@ -15,5 +14,4 @@ pub use extractors::*;
#[cfg(feature = "guards")] #[cfg(feature = "guards")]
pub use guard::*; pub use guard::*;
pub use headers::*; pub use headers::*;
#[cfg(feature = "responders")]
pub use responders::*; pub use responders::*;

View file

@ -1,16 +1,15 @@
//pub struct HxLocation() use std::convert::Infallible;
use std::{collections::HashMap, convert::Infallible};
use axum::{ use axum::{
http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri}, http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri},
response::{IntoResponse, IntoResponseParts, ResponseParts}, response::{IntoResponse, IntoResponseParts, ResponseParts},
}; };
use serde::Serialize;
use serde_json::Value;
use crate::headers; use crate::headers;
#[cfg(feature = "serde")]
pub mod serde;
const HX_SWAP_INNER_HTML: &str = "innerHTML"; const HX_SWAP_INNER_HTML: &str = "innerHTML";
const HX_SWAP_OUTER_HTML: &str = "outerHTML"; const HX_SWAP_OUTER_HTML: &str = "outerHTML";
const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin"; const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin";
@ -22,62 +21,20 @@ const HX_SWAP_NONE: &str = "none";
/// The `HX-Location` header. /// The `HX-Location` header.
/// ///
/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the pages location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history. /// This response header can be used to trigger a client side redirection without reloading the whole page.
/// ///
/// /// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127).
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone)]
pub struct HxLocation { pub struct HxLocation(Uri);
/// Url to load the response from
pub path: String,
/// The source element of the request
pub source: Option<String>,
/// An event that "triggered" the request
pub event: Option<String>,
/// A callback that will handle the response HTML
pub handler: Option<String>,
/// The target to swap the response into
pub target: Option<String>,
/// How the response will be swapped in relative to the target
pub swap: Option<SwapOption>,
/// Values to submit with the request
pub values: Option<Value>,
/// headers to submit with the request
pub headers: Option<Value>,
}
impl HxLocation {
pub fn from_uri(uri: &Uri) -> Self {
Self {
path: uri.to_string(),
source: None,
event: None,
handler: None,
target: None,
swap: None,
values: None,
headers: None,
}
}
}
impl IntoResponseParts for HxLocation { impl IntoResponseParts for HxLocation {
type Error = HxError; type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> { fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
let header_value = if self.source.is_none() res.headers_mut().insert(
&& self.event.is_none() headers::HX_LOCATION,
&& self.handler.is_none() HeaderValue::from_maybe_shared(self.0.to_string())?,
&& self.target.is_none() );
&& self.swap.is_none()
&& self.values.is_none()
&& self.headers.is_none()
{
HeaderValue::from_str(&self.path)?
} else {
HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)?
};
res.headers_mut().insert(headers::HX_LOCATION, header_value);
Ok(res) Ok(res)
} }
} }
@ -226,16 +183,23 @@ impl IntoResponseParts for HxReselect {
/// ///
/// Allows you to trigger client-side events. /// Allows you to trigger client-side events.
/// ///
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HxTrigger(Vec<HxEvent>); pub struct HxTrigger(Vec<String>);
impl IntoResponseParts for HxTrigger { impl IntoResponseParts for HxTrigger {
type Error = HxError; type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> { fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut() res.headers_mut().insert(
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?); headers::HX_TRIGGER,
HeaderValue::from_maybe_shared(
self.0
.into_iter()
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default(),
)?,
);
Ok(res) Ok(res)
} }
} }
@ -244,9 +208,9 @@ impl IntoResponseParts for HxTrigger {
/// ///
/// Allows you to trigger client-side events after the settle step. /// Allows you to trigger client-side events after the settle step.
/// ///
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HxTriggerAfterSettle(Vec<HxEvent>); pub struct HxTriggerAfterSettle(Vec<String>);
impl IntoResponseParts for HxTriggerAfterSettle { impl IntoResponseParts for HxTriggerAfterSettle {
type Error = HxError; type Error = HxError;
@ -254,7 +218,12 @@ impl IntoResponseParts for HxTriggerAfterSettle {
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> { fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert( res.headers_mut().insert(
headers::HX_TRIGGER_AFTER_SETTLE, headers::HX_TRIGGER_AFTER_SETTLE,
events_to_header_value(self.0)?, HeaderValue::from_maybe_shared(
self.0
.into_iter()
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default(),
)?,
); );
Ok(res) Ok(res)
} }
@ -264,9 +233,9 @@ impl IntoResponseParts for HxTriggerAfterSettle {
/// ///
/// Allows you to trigger client-side events after the swap step. /// Allows you to trigger client-side events after the swap step.
/// ///
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HxTriggerAfterSwap(Vec<HxEvent>); pub struct HxTriggerAfterSwap(Vec<String>);
impl IntoResponseParts for HxTriggerAfterSwap { impl IntoResponseParts for HxTriggerAfterSwap {
type Error = HxError; type Error = HxError;
@ -274,13 +243,19 @@ impl IntoResponseParts for HxTriggerAfterSwap {
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> { fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert( res.headers_mut().insert(
headers::HX_TRIGGER_AFTER_SWAP, headers::HX_TRIGGER_AFTER_SWAP,
events_to_header_value(self.0)?, HeaderValue::from_maybe_shared(
self.0
.into_iter()
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default(),
)?,
); );
Ok(res) Ok(res)
} }
} }
/// Values of the `hx-swap` attribute. /// Values of the `hx-swap` attribute.
// serde::Serialize is implemented in responders/serde.rs
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum SwapOption { pub enum SwapOption {
/// Replace the inner html of the target element. /// Replace the inner html of the target element.
@ -316,76 +291,10 @@ impl From<SwapOption> for HeaderValue {
} }
} }
// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485
// is implemented
impl Serialize for SwapOption {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
const UNIT_NAME: &str = "SwapOption";
match self {
Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML),
Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML),
Self::BeforeBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN)
}
Self::AfterBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN)
}
Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END),
Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END),
Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE),
Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct HxEvent {
pub name: String,
pub data: Option<Value>,
}
impl HxEvent {
pub fn new<T: Serialize>(name: String) -> Self {
Self { name, data: None }
}
pub fn new_with_data<T: Serialize>(name: String, data: T) -> Result<Self, serde_json::Error> {
let data = serde_json::to_value(data)?;
Ok(Self {
name,
data: Some(data),
})
}
}
pub(crate) fn events_to_header_value(events: Vec<HxEvent>) -> Result<HeaderValue, HxError> {
let with_data = events.iter().any(|e| e.data.is_some());
let header_value = if with_data {
// at least one event contains data so the header_value needs to be json encoded.
let header_value = events
.into_iter()
.map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default()))
.collect::<HashMap<_, _>>();
serde_json::to_string(&header_value)?
} else {
// no event contains data, the event names can be put in the header value separated
// by a comma.
events
.into_iter()
.map(|e| e.name)
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default()
};
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
}
pub enum HxError { pub enum HxError {
InvalidHeaderValue(InvalidHeaderValue), InvalidHeaderValue(InvalidHeaderValue),
#[cfg(feature = "serde")]
Serialization(serde_json::Error), Serialization(serde_json::Error),
} }
@ -395,6 +304,7 @@ impl From<InvalidHeaderValue> for HxError {
} }
} }
#[cfg(feature = "serde")]
impl From<serde_json::Error> for HxError { impl From<serde_json::Error> for HxError {
fn from(value: serde_json::Error) -> Self { fn from(value: serde_json::Error) -> Self {
Self::Serialization(value) Self::Serialization(value)
@ -407,6 +317,8 @@ impl IntoResponse for HxError {
Self::InvalidHeaderValue(_) => { Self::InvalidHeaderValue(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response() (StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response()
} }
#[cfg(feature = "serde")]
Self::Serialization(_) => ( Self::Serialization(_) => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"failed to serialize event", "failed to serialize event",

205
src/responders/serde.rs Normal file
View file

@ -0,0 +1,205 @@
use std::collections::HashMap;
use axum::{
http::{HeaderValue, Uri},
response::{IntoResponseParts, ResponseParts},
};
use serde::Serialize;
use serde_json::Value;
use crate::{
headers,
responders::{
HX_SWAP_AFTER_BEGIN, HX_SWAP_AFTER_END, HX_SWAP_BEFORE_BEGIN, HX_SWAP_BEFORE_END,
HX_SWAP_DELETE, HX_SWAP_INNER_HTML, HX_SWAP_NONE, HX_SWAP_OUTER_HTML,
},
HxError, SwapOption,
};
/// The `HX-Location` header.
///
/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the pages location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history.
///
/// Will fail if the supplied data contains or produces characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone, Serialize)]
pub struct HxLocation {
/// Url to load the response from
pub path: String,
/// The source element of the request
pub source: Option<String>,
/// An event that "triggered" the request
pub event: Option<String>,
/// A callback that will handle the response HTML
pub handler: Option<String>,
/// The target to swap the response into
pub target: Option<String>,
/// How the response will be swapped in relative to the target
pub swap: Option<SwapOption>,
/// Values to submit with the request
pub values: Option<Value>,
/// headers to submit with the request
pub headers: Option<Value>,
}
impl HxLocation {
pub fn from_uri(uri: &Uri) -> Self {
Self {
path: uri.to_string(),
source: None,
event: None,
handler: None,
target: None,
swap: None,
values: None,
headers: None,
}
}
}
impl IntoResponseParts for HxLocation {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
let header_value = if self.source.is_none()
&& self.event.is_none()
&& self.handler.is_none()
&& self.target.is_none()
&& self.swap.is_none()
&& self.values.is_none()
&& self.headers.is_none()
{
HeaderValue::from_str(&self.path)?
} else {
HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)?
};
res.headers_mut().insert(headers::HX_LOCATION, header_value);
Ok(res)
}
}
/// The `HX-Trigger` header.
///
/// Allows you to trigger client-side events.
///
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)]
pub struct HxTrigger(Vec<HxEvent>);
impl IntoResponseParts for HxTrigger {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut()
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
Ok(res)
}
}
/// The `HX-Trigger-After-Settle` header.
///
/// Allows you to trigger client-side events after the settle step.
///
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)]
pub struct HxTriggerAfterSettle(Vec<HxEvent>);
impl IntoResponseParts for HxTriggerAfterSettle {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_TRIGGER_AFTER_SETTLE,
events_to_header_value(self.0)?,
);
Ok(res)
}
}
/// The `HX-Trigger-After-Swap` header.
///
/// Allows you to trigger client-side events after the swap step.
///
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
#[derive(Debug, Clone)]
pub struct HxTriggerAfterSwap(Vec<HxEvent>);
impl IntoResponseParts for HxTriggerAfterSwap {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_TRIGGER_AFTER_SWAP,
events_to_header_value(self.0)?,
);
Ok(res)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct HxEvent {
pub name: String,
pub data: Option<Value>,
}
impl HxEvent {
pub fn new<T: Serialize>(name: String) -> Self {
Self { name, data: None }
}
pub fn new_with_data<T: Serialize>(name: String, data: T) -> Result<Self, serde_json::Error> {
let data = serde_json::to_value(data)?;
Ok(Self {
name,
data: Some(data),
})
}
}
pub(crate) fn events_to_header_value(events: Vec<HxEvent>) -> Result<HeaderValue, HxError> {
let with_data = events.iter().any(|e| e.data.is_some());
let header_value = if with_data {
// at least one event contains data so the header_value needs to be json encoded.
let header_value = events
.into_iter()
.map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default()))
.collect::<HashMap<_, _>>();
serde_json::to_string(&header_value)?
} else {
// no event contains data, the event names can be put in the header value separated
// by a comma.
events
.into_iter()
.map(|e| e.name)
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default()
};
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
}
// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485
// is implemented
impl serde::Serialize for SwapOption {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
const UNIT_NAME: &str = "SwapOption";
match self {
Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML),
Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML),
Self::BeforeBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN)
}
Self::AfterBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN)
}
Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END),
Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END),
Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE),
Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE),
}
}
}