use futures::future::LocalBoxFuture;
use lenso_kernel::InvocationContext;
use serde::de::DeserializeOwned;
use crate::{EndpointHandleInvocationError, HandleRequest, HandleResponse, response};
#[derive(Debug)]
pub enum ExtractorRejection {
Response(HandleResponse),
Invocation(EndpointHandleInvocationError),
}
impl From<HandleResponse> for ExtractorRejection {
fn from(response: HandleResponse) -> Self {
Self::Response(response)
}
}
impl From<EndpointHandleInvocationError> for ExtractorRejection {
fn from(error: EndpointHandleInvocationError) -> Self {
Self::Invocation(error)
}
}
impl From<response::ResponseBuildError> for ExtractorRejection {
fn from(error: response::ResponseBuildError) -> Self {
Self::Invocation(error.into())
}
}
pub type ExtractorFuture<'a, T> = LocalBoxFuture<'a, Result<T, ExtractorRejection>>;
pub trait FromRequest<P: ?Sized>: Sized {
fn from_request<'a>(
provider: &'a P,
context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Json<T>(pub T);
impl<P, T> FromRequest<P> for Json<T>
where
P: ?Sized,
T: DeserializeOwned + 'static,
{
fn from_request<'a>(
_provider: &'a P,
_context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self> {
Box::pin(futures::future::ready(extract_json(request)))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Path<T>(pub T);
impl<P, T> FromRequest<P> for Path<T>
where
P: ?Sized,
T: DeserializeOwned + 'static,
{
fn from_request<'a>(
_provider: &'a P,
_context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self> {
Box::pin(futures::future::ready(extract_path(request)))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Query<T>(pub T);
impl<P, T> FromRequest<P> for Query<T>
where
P: ?Sized,
T: DeserializeOwned + 'static,
{
fn from_request<'a>(
_provider: &'a P,
_context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self> {
Box::pin(futures::future::ready(extract_query(request)))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestId(pub String);
impl<P> FromRequest<P> for RequestId
where
P: ?Sized,
{
fn from_request<'a>(
_provider: &'a P,
_context: &'a mut InvocationContext,
request: &'a HandleRequest,
) -> ExtractorFuture<'a, Self> {
Box::pin(futures::future::ready(Ok(Self(request.request_id.clone()))))
}
}
fn extract_json<T>(request: &HandleRequest) -> Result<Json<T>, ExtractorRejection>
where
T: DeserializeOwned,
{
if !has_json_content_type(request) {
return Err(response::problem(
response::StatusCode::UNSUPPORTED_MEDIA_TYPE,
"json_content_type_required",
"The request content type must be application/json.",
)
.into());
}
serde_json::from_slice(request.body.as_ref())
.map(Json)
.map_err(|_| {
response::problem(
response::StatusCode::BAD_REQUEST,
"invalid_json_body",
"The request body is not valid JSON for this endpoint.",
)
.into()
})
}
fn extract_path<T>(request: &HandleRequest) -> Result<Path<T>, ExtractorRejection>
where
T: DeserializeOwned,
{
let parameters = request
.path_parameters
.iter()
.map(|parameter| (parameter.name.as_str(), parameter.value.as_str()))
.collect::<Vec<_>>();
let encoded = serde_urlencoded::to_string(parameters).map_err(|_| invalid_path())?;
serde_urlencoded::from_str(&encoded)
.map(Path)
.map_err(|_| invalid_path())
}
fn extract_query<T>(request: &HandleRequest) -> Result<Query<T>, ExtractorRejection>
where
T: DeserializeOwned,
{
serde_urlencoded::from_str(request.query.as_deref().unwrap_or_default())
.map(Query)
.map_err(|_| {
response::problem(
response::StatusCode::BAD_REQUEST,
"invalid_query",
"The query string is not valid for this endpoint.",
)
.into()
})
}
fn has_json_content_type(request: &HandleRequest) -> bool {
request.headers.iter().any(|header| {
header.name.eq_ignore_ascii_case("content-type")
&& header
.value
.split(';')
.next()
.is_some_and(|value| value.trim().eq_ignore_ascii_case("application/json"))
})
}
fn invalid_path() -> ExtractorRejection {
response::problem(
response::StatusCode::BAD_REQUEST,
"invalid_path_parameters",
"The route path parameters are not valid for this endpoint.",
)
.into()
}