use cookie::Cookie;
use http::StatusCode;
use serde::Serialize;
use serde_json::value::RawValue;
use crate::document::{DocumentParts, HeadRules, PreparedDocument};
use crate::finite::check_finite_floats;
use crate::result::{
ApiEndpointRedirect, ApiEndpointResult, ApiEndpointTarget, Continue, Failure, FailureKind,
HeaderChange, NotModified, PageTarget, Redirect, RedirectStatus, RedirectTarget, Render,
Respond, ResponseMeta, TerminalOutcome,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ApiEndpointTypes {
pub(crate) input: std::any::TypeId,
pub(crate) output: std::any::TypeId,
pub(crate) declared_error: std::any::TypeId,
}
#[derive(Clone, Debug)]
pub(crate) struct RedirectingApiEndpoint {
pub(crate) method: http::Method,
pub(crate) types: ApiEndpointTypes,
}
pub(crate) trait ResolveTarget: Send + Sync {
fn resolve_page_target(&self, target: &PageTarget) -> Result<String, String>;
fn resolve_api_endpoint_target(
&self,
redirecting: &RedirectingApiEndpoint,
target: &ApiEndpointTarget,
status: RedirectStatus,
) -> Result<String, String>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RedirectKind {
Nav,
ApiEndpoint,
}
#[derive(Clone, Debug)]
pub(crate) struct Redirection {
pub(crate) location: http::HeaderValue,
pub(crate) status: RedirectStatus,
pub(crate) kind: RedirectKind,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Contribution {
pub(crate) status: Option<StatusCode>,
pub(crate) header_changes: Vec<HeaderChange>,
pub(crate) cookies: Vec<Cookie<'static>>,
pub(crate) document: PreparedDocument,
}
impl Contribution {
pub(crate) fn merge(&mut self, later: Self) {
if later.status.is_some() {
self.status = later.status;
}
self.header_changes.extend(later.header_changes);
for cookie in later.cookies {
self.cookies.retain(|existing| {
!primitives_rs::http::cookies::headers::same_cookie_identity(existing, &cookie)
});
self.cookies.push(cookie);
}
self.document.merge(later.document);
}
}
fn check_success_status(status: StatusCode, is_page: bool) -> Result<(), String> {
if !status.is_success() {
return Err(format!(
"status {} is outside 200 through 299, which a success requires",
status.as_u16()
));
}
if is_page && matches!(status, StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT) {
return Err(format!(
"status {} forbids a body, and a page response always has one",
status.as_u16()
));
}
Ok(())
}
fn check_failure_status(status: StatusCode) -> Result<(), String> {
if !(status.is_client_error() || status.is_server_error()) {
return Err(format!(
"status {} is outside 400 through 599, which a failure requires",
status.as_u16()
));
}
Ok(())
}
fn to_contribution(meta: ResponseMeta, document: PreparedDocument) -> Result<Contribution, String> {
if let Some(problem) = meta.invalid_header {
return Err(format!("a header is invalid: {problem}"));
}
if let Some(problem) = meta.invalid_cookie {
return Err(format!("a cookie is invalid: {problem}"));
}
Ok(Contribution {
status: meta.status,
header_changes: meta.header_changes,
cookies: meta.cookies,
document,
})
}
#[derive(Debug, Serialize)]
pub(crate) enum PublicError {
#[serde(rename = "declared_error")]
Declared(Box<RawValue>),
#[serde(rename = "rejection")]
Rejection(Box<RawValue>),
}
#[derive(Clone, Copy)]
enum DeclaredErrorKind {
Declared,
Rejection,
}
#[derive(Debug)]
pub(crate) enum Outcome {
Continue {
contribution: Contribution,
output: Option<Box<RawValue>>,
},
NotModified { contribution: Contribution },
Redirect {
contribution: Contribution,
redirection: Redirection,
},
Failure {
contribution: Contribution,
status: StatusCode,
public_error: Option<PublicError>,
},
}
impl Outcome {
pub(crate) fn is_terminal(&self) -> bool {
!matches!(self, Self::Continue { .. } | Self::NotModified { .. })
}
pub(crate) fn internal_server_error() -> Self {
Self::Failure {
contribution: Contribution::default(),
status: StatusCode::INTERNAL_SERVER_ERROR,
public_error: None,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct HandlerLabel {
pub(crate) kind: HandlerKind,
pub(crate) pattern: std::sync::Arc<str>,
pub(crate) request_path: std::sync::Arc<str>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum HandlerKind {
TaskMiddleware,
ViewLoader,
ApiEndpoint,
}
impl HandlerKind {
pub(crate) fn span_name(self) -> &'static str {
match self {
Self::TaskMiddleware => "vorma.task_middleware",
Self::ViewLoader => "vorma.view_loader",
Self::ApiEndpoint => "vorma.api_endpoint",
}
}
}
pub(crate) fn invalid_input_outcome() -> Outcome {
Outcome::Failure {
contribution: Contribution::default(),
status: StatusCode::BAD_REQUEST,
public_error: None,
}
}
pub(crate) fn invalid_server_result_outcome(label: &HandlerLabel, problem: &str) -> Outcome {
tracing::error!(
handler = label.kind.span_name(),
pattern = &*label.pattern,
path = &*label.request_path,
problem,
"Vorma received an invalid server result",
);
Outcome::internal_server_error()
}
fn serialize_output<Output: Serialize + 'static>(
output: &Output,
) -> Result<Option<Box<RawValue>>, String> {
if std::any::TypeId::of::<Output>() == std::any::TypeId::of::<()>() {
return Ok(None);
}
check_finite_floats(output).map_err(|problem| format!("the output is invalid: {problem}"))?;
serde_json::value::to_raw_value(output)
.map(Some)
.map_err(|error| format!("output cannot be serialized: {error}"))
}
fn nav_redirect_outcome(
label: &HandlerLabel,
redirect: Redirect,
resolver: &dyn ResolveTarget,
) -> Outcome {
let location = match &redirect.target {
RedirectTarget::Url(url) => Ok(url.clone()),
RedirectTarget::Page(target) => resolver.resolve_page_target(target).map_err(|problem| {
format!(
"the redirect's page target {:?} is invalid: {problem}",
target.pattern
)
}),
};
redirect_outcome(
label,
location,
RedirectParts {
status: redirect.status,
meta: redirect.meta,
kind: RedirectKind::Nav,
},
)
}
fn api_endpoint_redirect_outcome(
label: &HandlerLabel,
redirecting: &RedirectingApiEndpoint,
redirect: ApiEndpointRedirect,
resolver: &dyn ResolveTarget,
) -> Outcome {
let location = resolver
.resolve_api_endpoint_target(redirecting, &redirect.target, redirect.status)
.map_err(|problem| {
format!(
"the redirect's endpoint target {:?} is invalid: {problem}",
redirect.target.pattern
)
});
redirect_outcome(
label,
location,
RedirectParts {
status: redirect.status,
meta: redirect.meta,
kind: RedirectKind::ApiEndpoint,
},
)
}
struct RedirectParts {
status: RedirectStatus,
meta: ResponseMeta,
kind: RedirectKind,
}
fn redirect_outcome(
label: &HandlerLabel,
location: Result<String, String>,
redirect: RedirectParts,
) -> Outcome {
let location = location.and_then(|location| {
http::HeaderValue::try_from(location)
.map_err(|_| "the redirect location is not a valid header value".to_owned())
});
let location = match location {
Ok(location) => location,
Err(problem) => return invalid_server_result_outcome(label, &problem),
};
let mut meta = redirect.meta;
meta.status = Some(redirect.status.to_status_code());
match to_contribution(meta, PreparedDocument::default()) {
Ok(contribution) => Outcome::Redirect {
contribution,
redirection: Redirection {
location,
status: redirect.status,
kind: redirect.kind,
},
},
Err(problem) => invalid_server_result_outcome(label, &problem),
}
}
fn prepare_document(
document: DocumentParts,
head_rules: Option<&HeadRules>,
) -> Result<PreparedDocument, String> {
let Some(head_rules) = head_rules else {
return Ok(PreparedDocument::default());
};
document
.prepare(head_rules)
.map_err(|error| error.to_string())
}
fn continue_outcome<Output: Serialize + 'static>(
label: &HandlerLabel,
output: Option<&Output>,
document: DocumentParts,
meta: ResponseMeta,
head_rules: Option<&HeadRules>,
) -> Outcome {
if let Some(status) = meta.status
&& let Err(problem) = check_success_status(status, head_rules.is_some())
{
return invalid_server_result_outcome(label, &problem);
}
let output = match output.map(serialize_output).transpose() {
Ok(output) => output.flatten(),
Err(problem) => return invalid_server_result_outcome(label, &problem),
};
let document = match prepare_document(document, head_rules) {
Ok(document) => document,
Err(problem) => return invalid_server_result_outcome(label, &problem),
};
match to_contribution(meta, document) {
Ok(contribution) => Outcome::Continue {
contribution,
output,
},
Err(problem) => invalid_server_result_outcome(label, &problem),
}
}
fn terminal_outcome<DeclaredError: Serialize>(
label: &HandlerLabel,
declared_error_kind: DeclaredErrorKind,
terminal: TerminalOutcome<DeclaredError>,
resolver: &dyn ResolveTarget,
head_rules: Option<&HeadRules>,
) -> Outcome {
match terminal {
TerminalOutcome::Redirect(redirect) => nav_redirect_outcome(label, *redirect, resolver),
TerminalOutcome::Failure(failure) => {
from_failure(label, declared_error_kind, *failure, head_rules)
}
}
}
pub(crate) fn from_view_loader_result<Output: Serialize + 'static, DeclaredError: Serialize>(
label: &HandlerLabel,
result: Result<Render<Output>, TerminalOutcome<DeclaredError>>,
resolver: &dyn ResolveTarget,
head_rules: Option<&HeadRules>,
) -> Outcome {
match result {
Ok(Render {
output,
document,
meta,
}) => continue_outcome(label, Some(&output), document, meta, head_rules),
Err(terminal) => terminal_outcome(
label,
DeclaredErrorKind::Declared,
terminal,
resolver,
head_rules,
),
}
}
pub(crate) fn from_api_endpoint_result<Output: Serialize + 'static, DeclaredError: Serialize>(
label: &HandlerLabel,
redirecting: &RedirectingApiEndpoint,
result: Result<ApiEndpointResult<Output>, TerminalOutcome<DeclaredError>>,
resolver: &dyn ResolveTarget,
head_rules: Option<&HeadRules>,
) -> Outcome {
match result {
Ok(ApiEndpointResult::Respond(Respond { output, meta })) => continue_outcome(
label,
Some(&output),
DocumentParts::default(),
meta,
head_rules,
),
Ok(ApiEndpointResult::Redirect(redirect)) => {
api_endpoint_redirect_outcome(label, redirecting, redirect, resolver)
}
Ok(ApiEndpointResult::NotModified(NotModified { mut meta })) => {
if redirecting.method != http::Method::GET {
return invalid_server_result_outcome(
label,
"NotModified is valid only for a GET endpoint",
);
}
meta.status = Some(StatusCode::NOT_MODIFIED);
match to_contribution(meta, PreparedDocument::default()) {
Ok(contribution) => Outcome::NotModified { contribution },
Err(problem) => invalid_server_result_outcome(label, &problem),
}
}
Err(terminal) => terminal_outcome(
label,
DeclaredErrorKind::Declared,
terminal,
resolver,
head_rules,
),
}
}
pub(crate) fn from_task_middleware_result<DeclaredError: Serialize>(
label: &HandlerLabel,
result: Result<Continue, TerminalOutcome<DeclaredError>>,
resolver: &dyn ResolveTarget,
head_rules: Option<&HeadRules>,
) -> Outcome {
match result {
Ok(Continue { document, meta }) => {
continue_outcome::<()>(label, None, document, meta, head_rules)
}
Err(terminal) => terminal_outcome(
label,
DeclaredErrorKind::Rejection,
terminal,
resolver,
head_rules,
),
}
}
fn from_failure<DeclaredError: Serialize>(
label: &HandlerLabel,
declared_error_kind: DeclaredErrorKind,
failure: Failure<DeclaredError>,
head_rules: Option<&HeadRules>,
) -> Outcome {
let Failure {
kind,
status,
document,
meta,
} = failure;
if let FailureKind::Invalid(problem) = &kind {
return invalid_server_result_outcome(label, problem);
}
if let Err(problem) = check_failure_status(status) {
return invalid_server_result_outcome(
label,
&format!("failure metadata is invalid: {problem}"),
);
}
let document = match prepare_document(document, head_rules) {
Ok(document) => document,
Err(problem) => {
return invalid_server_result_outcome(
label,
&format!("failure metadata is invalid: {problem}"),
);
}
};
let contribution = match to_contribution(meta, document) {
Ok(contribution) => contribution,
Err(problem) => {
return invalid_server_result_outcome(
label,
&format!("failure metadata is invalid: {problem}"),
);
}
};
let public_error = match kind {
FailureKind::Declared(error) => match check_finite_floats(&error)
.map_err(|problem| problem.to_string())
.and_then(|()| {
serde_json::value::to_raw_value(&error).map_err(|error| error.to_string())
}) {
Ok(public_error) => {
tracing::debug!(
handler = label.kind.span_name(),
pattern = &*label.pattern,
path = &*label.request_path,
status = status.as_u16(),
error = public_error.get(),
"Vorma handler failed with a declared error",
);
Some(match declared_error_kind {
DeclaredErrorKind::Declared => PublicError::Declared(public_error),
DeclaredErrorKind::Rejection => PublicError::Rejection(public_error),
})
}
Err(problem) => {
return invalid_server_result_outcome(
label,
&format!("the declared error is invalid: {problem}"),
);
}
},
FailureKind::Invalid(_) => unreachable!("an invalid failure returned earlier"),
FailureKind::Undeclared(error) => {
tracing::error!(
handler = label.kind.span_name(),
pattern = &*label.pattern,
path = &*label.request_path,
status = status.as_u16(),
error = &*error as &(dyn std::error::Error + 'static),
"Vorma handler failed",
);
None
}
};
Outcome::Failure {
contribution,
status,
public_error,
}
}