use std::any::TypeId;
use std::fmt;
use std::future::Future;
use std::panic::Location;
use std::sync::Arc;
use bytes::Bytes;
use futures_util::future::{self, BoxFuture, FutureExt};
use http::Method;
use primitives_rs::tasks::TaskCtx;
use primitives_rs::tsgen::{self, TsGen, TypePhase, TypeRef, TypeRegistry};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::ctx::{ApiEndpointCtx, HandlerFacts, RequestFacts, TaskMiddlewareCtx, ViewLoaderCtx};
use crate::form::{FormBody, FormDecodeError, FormInput, decode_form_input};
use crate::media_type::FormMediaType;
use crate::outcome::{
ApiEndpointTypes, HandlerLabel, Outcome, RedirectingApiEndpoint, from_api_endpoint_result,
from_task_middleware_result, from_view_loader_result, invalid_input_outcome,
invalid_server_result_outcome,
};
use crate::pattern::{ApiPatternParams, DecodedMatch, ViewPatternParams};
use crate::result::{ApiEndpointResult, Continue, Render, RequestRejected, TerminalOutcome};
use crate::search::{SearchShape, SearchShapeError, derive_search_shape};
pub(crate) struct Invocation<State, Input> {
pub(crate) state: Arc<State>,
pub(crate) facts: Arc<RequestFacts>,
pub(crate) task_ctx: TaskCtx<State>,
pub(crate) decoded_match: DecodedMatch,
pub(crate) input: Input,
pub(crate) label: HandlerLabel,
}
type HandlerInvoker<Input> = Arc<dyn Fn(Input) -> BoxFuture<'static, Outcome> + Send + Sync>;
pub(crate) struct HandlerTypeRefs {
pub(crate) input: Option<TypeRef>,
pub(crate) output: Option<TypeRef>,
pub(crate) errors: TypeRef,
}
type CollectTypes = fn(&mut TypeRegistry) -> tsgen::Result<HandlerTypeRefs>;
fn collect_types<Input: TsGen, Output: TsGen, DeclaredError: TsGen>(
registry: &mut TypeRegistry,
) -> tsgen::Result<HandlerTypeRefs> {
Input::collect_type_defs_for(TypePhase::Deserialize, registry)?;
Output::collect_type_defs_for(TypePhase::Serialize, registry)?;
DeclaredError::collect_type_defs_for(TypePhase::Serialize, registry)?;
Ok(HandlerTypeRefs {
input: (!is_unit::<Input>()).then(|| Input::type_ref_for(TypePhase::Deserialize)),
output: (!is_unit::<Output>()).then(|| Output::type_ref_for(TypePhase::Serialize)),
errors: DeclaredError::type_ref_for(TypePhase::Serialize),
})
}
fn is_unit<Value: 'static>() -> bool {
TypeId::of::<Value>() == TypeId::of::<()>()
}
fn derive_view_search_shape<Input: TsGen + 'static>()
-> Result<Option<SearchShape>, SearchShapeError> {
if is_unit::<Input>() {
return Ok(None);
}
derive_search_shape::<Input>().map(Some)
}
fn is_search_input_valid<Input: DeserializeOwned>(value: serde_json::Value) -> bool {
serde_json::from_value::<Input>(value).is_ok()
}
impl<State> ViewLoader<State> {
pub(crate) fn describe(&self) -> String {
format!("{} view_loaders {:?}", self.location, self.pattern)
}
}
impl<State> ApiEndpoint<State> {
pub(crate) fn describe(&self) -> String {
format!(
"{} api_endpoints {} {:?}",
self.location, self.method, self.pattern
)
}
}
impl<State> TaskMiddleware<State> {
pub(crate) fn describe(&self) -> String {
format!("{} task_middlewares", self.location)
}
}
pub struct ViewLoader<State> {
pub(crate) location: &'static Location<'static>,
pub(crate) pattern: &'static str,
pub(crate) derive_search_shape_fn: fn() -> Result<Option<SearchShape>, SearchShapeError>,
pub(crate) is_search_input_valid_fn: fn(serde_json::Value) -> bool,
pub(crate) collect_types_fn: CollectTypes,
pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, serde_json::Value>>,
}
impl<State> Clone for ViewLoader<State> {
fn clone(&self) -> Self {
Self {
location: self.location,
pattern: self.pattern,
derive_search_shape_fn: self.derive_search_shape_fn,
is_search_input_valid_fn: self.is_search_input_valid_fn,
collect_types_fn: self.collect_types_fn,
invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
}
}
}
impl<State> fmt::Debug for ViewLoader<State> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("ViewLoader")
.field(&self.pattern)
.finish()
}
}
impl<State: Send + Sync + 'static> ViewLoader<State> {
#[track_caller]
pub fn new<Params, Input, Output, DeclaredError, HandlerFn, Fut>(handler: HandlerFn) -> Self
where
Params: ViewPatternParams,
Input: DeserializeOwned + TsGen,
Output: Serialize + TsGen,
DeclaredError: Serialize + TsGen,
HandlerFn: Fn(ViewLoaderCtx<State, Params, Input>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Render<Output>, TerminalOutcome<DeclaredError>>>
+ Send
+ 'static,
{
let pattern = Params::PATTERN;
let handler = Arc::new(handler);
let invoke_handler =
move |invocation: Invocation<State, serde_json::Value>| -> BoxFuture<'static, Outcome> {
let handler = Arc::clone(&handler);
async move {
let label = invocation.label;
let params = match Params::from_decoded_match(invocation.decoded_match) {
Ok(params) => params,
Err(error) => {
return invalid_server_result_outcome(&label, &error.to_string());
}
};
let Ok(input) = serde_json::from_value::<Input>(invocation.input) else {
return invalid_input_outcome();
};
let facts = invocation.facts;
let ctx = facts.to_view_loader_ctx(HandlerFacts {
state: invocation.state,
params,
input,
pattern,
task_ctx: invocation.task_ctx,
});
from_view_loader_result(
&label,
handler(ctx).await,
&*facts.target_resolver,
facts.head_rules.as_deref(),
)
}
.boxed()
};
Self {
location: Location::caller(),
pattern,
derive_search_shape_fn: derive_view_search_shape::<Input>,
is_search_input_valid_fn: is_search_input_valid::<Input>,
collect_types_fn: collect_types::<Input, Output, DeclaredError>,
invoke_handler_fn: Arc::new(invoke_handler),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ApiEndpointKind {
Query,
Mutation,
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ApiEndpointInputKind {
None,
Search,
Json,
FormData,
}
#[doc(hidden)]
pub enum ApiEndpointInputSource {
None,
Search(serde_json::Value),
Json(Bytes),
FormData {
media_type: FormMediaType,
body: FormBody,
},
}
#[doc(hidden)]
#[derive(Debug)]
pub enum InputError {
Invalid,
UnsupportedMediaType,
BodyRead(Box<dyn std::error::Error + Send + Sync>),
Storage(std::io::Error),
}
impl From<FormDecodeError> for InputError {
fn from(error: FormDecodeError) -> Self {
match error {
FormDecodeError::Invalid => Self::Invalid,
FormDecodeError::BodyRead(error) => Self::BodyRead(error),
FormDecodeError::Storage(error) => Self::Storage(error),
}
}
}
pub(crate) fn input_failure_outcome(label: &HandlerLabel, error: InputError) -> Outcome {
match error {
InputError::Invalid => invalid_input_outcome(),
InputError::UnsupportedMediaType => Outcome::Failure {
contribution: crate::outcome::Contribution::default(),
status: http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
public_error: None,
},
InputError::BodyRead(error) => {
let status = if is_length_limit_error(&*error) {
http::StatusCode::PAYLOAD_TOO_LARGE
} else {
http::StatusCode::BAD_REQUEST
};
Outcome::Failure {
contribution: crate::outcome::Contribution::default(),
status,
public_error: None,
}
}
InputError::Storage(error) => {
tracing::error!(
handler = label.kind.span_name(),
pattern = &*label.pattern,
path = &*label.request_path,
error = &error as &(dyn std::error::Error + 'static),
"Vorma could not store an uploaded file",
);
Outcome::internal_server_error()
}
}
}
fn is_length_limit_error(error: &(dyn std::error::Error + 'static)) -> bool {
let mut current = Some(error);
while let Some(error) = current {
if error.is::<http_body_util::LengthLimitError>() {
return true;
}
current = error.source();
}
false
}
pub trait ApiEndpointInput: TsGen + Sized + Send + 'static {
#[doc(hidden)]
fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str>;
#[doc(hidden)]
fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>>;
#[doc(hidden)]
fn __is_search_input_valid(value: serde_json::Value) -> bool;
}
impl<Input: DeserializeOwned + TsGen + Send + 'static> ApiEndpointInput for Input {
fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str> {
Ok(if is_unit::<Input>() {
ApiEndpointInputKind::None
} else if method == Method::GET {
ApiEndpointInputKind::Search
} else {
ApiEndpointInputKind::Json
})
}
fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>> {
let decoded = match source {
ApiEndpointInputSource::None => {
serde_json::from_value(serde_json::Value::Null).map_err(|_| InputError::Invalid)
}
ApiEndpointInputSource::Search(value) => {
serde_json::from_value(value).map_err(|_| InputError::Invalid)
}
ApiEndpointInputSource::Json(bytes) => {
serde_json::from_slice(&bytes).map_err(|_| InputError::Invalid)
}
ApiEndpointInputSource::FormData { .. } => Err(InputError::Invalid),
};
future::ready(decoded).boxed()
}
fn __is_search_input_valid(value: serde_json::Value) -> bool {
is_search_input_valid::<Input>(value)
}
}
impl<Form: DeserializeOwned + TsGen> ApiEndpointInput for FormInput<Form> {
fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str> {
if method == Method::GET {
return Err("a GET endpoint reads search parameters, so it cannot take form input");
}
Ok(ApiEndpointInputKind::FormData)
}
fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>> {
async move {
let ApiEndpointInputSource::FormData { media_type, body } = source else {
return Err(InputError::Invalid);
};
Ok(FormInput(
decode_form_input::<Form>(media_type, body).await?,
))
}
.boxed()
}
fn __is_search_input_valid(_value: serde_json::Value) -> bool {
false
}
}
pub struct ApiEndpoint<State> {
pub(crate) location: &'static Location<'static>,
pub(crate) kind: ApiEndpointKind,
pub(crate) method: Method,
pub(crate) pattern: &'static str,
pub(crate) determine_input_kind_fn: fn(&Method) -> Result<ApiEndpointInputKind, &'static str>,
pub(crate) derive_search_shape_fn: fn() -> Result<SearchShape, SearchShapeError>,
pub(crate) is_search_input_valid_fn: fn(serde_json::Value) -> bool,
pub(crate) max_request_body_size: Option<usize>,
pub(crate) types: ApiEndpointTypes,
pub(crate) collect_types_fn: CollectTypes,
pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, ApiEndpointInputSource>>,
}
impl<State> Clone for ApiEndpoint<State> {
fn clone(&self) -> Self {
Self {
location: self.location,
kind: self.kind,
method: self.method.clone(),
pattern: self.pattern,
determine_input_kind_fn: self.determine_input_kind_fn,
derive_search_shape_fn: self.derive_search_shape_fn,
is_search_input_valid_fn: self.is_search_input_valid_fn,
max_request_body_size: self.max_request_body_size,
types: self.types,
collect_types_fn: self.collect_types_fn,
invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
}
}
}
impl<State> fmt::Debug for ApiEndpoint<State> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ApiEndpoint")
.field("kind", &self.kind)
.field("method", &self.method)
.field("pattern", &self.pattern)
.finish()
}
}
impl<State: Send + Sync + 'static> ApiEndpoint<State> {
#[track_caller]
pub fn new<Params, Input, Output, DeclaredError, HandlerFn, Fut>(
kind: ApiEndpointKind,
method: Method,
handler: HandlerFn,
) -> Self
where
Params: ApiPatternParams,
Input: ApiEndpointInput,
Output: Serialize + TsGen + 'static,
DeclaredError: Serialize + TsGen + 'static,
HandlerFn: Fn(ApiEndpointCtx<State, Params, Input>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ApiEndpointResult<Output>, TerminalOutcome<DeclaredError>>>
+ Send
+ 'static,
{
let types = ApiEndpointTypes {
input: TypeId::of::<Input>(),
output: TypeId::of::<Output>(),
declared_error: TypeId::of::<DeclaredError>(),
};
let redirecting = Arc::new(RedirectingApiEndpoint {
method: method.clone(),
types,
});
let pattern = Params::PATTERN;
let handler = Arc::new(handler);
let invoke_handler = move |invocation: Invocation<State, ApiEndpointInputSource>| -> BoxFuture<'static, Outcome> {
let redirecting = Arc::clone(&redirecting);
let handler = Arc::clone(&handler);
async move {
let label = invocation.label;
let params = match Params::from_decoded_match(invocation.decoded_match) {
Ok(params) => params,
Err(error) => return invalid_server_result_outcome(&label, &error.to_string()),
};
let input = match Input::__decode(invocation.input).await {
Ok(input) => input,
Err(error) => return input_failure_outcome(&label, error),
};
let ctx = invocation.facts.to_api_endpoint_ctx(HandlerFacts {
state: invocation.state,
params,
input,
pattern,
task_ctx: invocation.task_ctx,
});
from_api_endpoint_result(
&label,
&redirecting,
handler(ctx).await,
&*invocation.facts.target_resolver,
invocation.facts.head_rules.as_deref(),
)
}
.boxed()
};
Self {
location: Location::caller(),
kind,
method,
pattern,
determine_input_kind_fn: Input::__input_kind,
derive_search_shape_fn: derive_search_shape::<Input>,
is_search_input_valid_fn: Input::__is_search_input_valid,
max_request_body_size: None,
types,
collect_types_fn: collect_types::<Input, Output, DeclaredError>,
invoke_handler_fn: Arc::new(invoke_handler),
}
}
pub fn set_max_request_body_size(mut self, bytes: usize) -> Self {
self.max_request_body_size = Some(bytes);
self
}
}
pub struct TaskMiddleware<State> {
pub(crate) location: &'static Location<'static>,
pub(crate) patterns: Vec<String>,
pub(crate) methods: Vec<Method>,
pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, ()>>,
}
impl<State> Clone for TaskMiddleware<State> {
fn clone(&self) -> Self {
Self {
location: self.location,
patterns: self.patterns.clone(),
methods: self.methods.clone(),
invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
}
}
}
impl<State> fmt::Debug for TaskMiddleware<State> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TaskMiddleware")
.field("patterns", &self.patterns)
.field("methods", &self.methods)
.finish()
}
}
impl<State: Send + Sync + 'static> TaskMiddleware<State> {
#[track_caller]
pub fn new<HandlerFn, Fut>(handler: HandlerFn) -> Self
where
HandlerFn: Fn(TaskMiddlewareCtx<State>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Continue, TerminalOutcome<RequestRejected>>> + Send + 'static,
{
let handler = Arc::new(handler);
let invoke_handler =
move |invocation: Invocation<State, ()>| -> BoxFuture<'static, Outcome> {
let Invocation {
state,
facts,
task_ctx,
label,
..
} = invocation;
let handler = Arc::clone(&handler);
async move {
let ctx = facts.to_task_middleware_ctx(state, task_ctx);
from_task_middleware_result(
&label,
handler(ctx).await,
&*facts.target_resolver,
facts.head_rules.as_deref(),
)
}
.boxed()
};
Self {
location: Location::caller(),
patterns: Vec::new(),
methods: Vec::new(),
invoke_handler_fn: Arc::new(invoke_handler),
}
}
pub fn set_patterns<Pattern: Into<String>>(
mut self,
patterns: impl IntoIterator<Item = Pattern>,
) -> Self {
self.patterns = patterns.into_iter().map(Into::into).collect();
self
}
pub fn set_methods(mut self, methods: impl IntoIterator<Item = Method>) -> Self {
self.methods = methods.into_iter().collect();
self
}
}