use std::any::{TypeId, type_name};
use std::error::Error as StdError;
use std::fmt;
use cookie::Cookie;
use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use primitives_rs::tsgen::{FieldDef, FieldPresence, TsGen, TypeDef, TypeRef, TypeRegistry};
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use crate::document::{AttrValue, DocumentParts, HeadEl, HeadItem};
use crate::pattern::{ApiPatternParams, ViewPatternParams};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum HeaderChange {
Set {
name: HeaderName,
value: HeaderValue,
},
Add {
name: HeaderName,
value: HeaderValue,
},
}
impl HeaderChange {
pub(crate) fn apply_to(&self, headers: &mut HeaderMap) {
match self {
Self::Set { name, value } => {
headers.insert(name.clone(), value.clone());
}
Self::Add { name, value } => {
headers.append(name.clone(), value.clone());
}
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ResponseMeta {
pub(crate) status: Option<StatusCode>,
pub(crate) header_changes: Vec<HeaderChange>,
pub(crate) cookies: Vec<Cookie<'static>>,
pub(crate) invalid_header: Option<String>,
pub(crate) invalid_cookie: Option<String>,
}
impl ResponseMeta {
fn change_header<Name, Value>(
&mut self,
name: Name,
value: Value,
create_header_change_fn: fn(HeaderName, HeaderValue) -> HeaderChange,
) where
Name: TryInto<HeaderName>,
<Name as TryInto<HeaderName>>::Error: Into<http::Error>,
Value: TryInto<HeaderValue>,
<Value as TryInto<HeaderValue>>::Error: Into<http::Error>,
{
let name = match name.try_into() {
Ok(name) => name,
Err(error) => return self.record_invalid_header(format!("{}", error.into())),
};
match value.try_into() {
Ok(value) => self
.header_changes
.push(create_header_change_fn(name, value)),
Err(error) => {
self.record_invalid_header(format!("header {name}: {}", error.into()));
}
}
}
fn record_invalid_header(&mut self, problem: String) {
if self.invalid_header.is_none() {
self.invalid_header = Some(problem);
}
}
fn headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
for change in &self.header_changes {
change.apply_to(&mut headers);
}
headers
}
fn set_cookie(&mut self, cookie: Cookie<'static>) {
match primitives_rs::http::cookies::headers::validate_set_cookie(&cookie)
.map_err(|error| error.to_string())
{
Ok(()) => {
self.cookies.retain(|existing| {
!primitives_rs::http::cookies::headers::same_cookie_identity(existing, &cookie)
});
self.cookies.push(cookie);
}
Err(problem) => {
if self.invalid_cookie.is_none() {
self.invalid_cookie = Some(problem);
}
}
}
}
}
macro_rules! response_meta_methods {
($type:ty $(, $generic:ident)?) => {
impl$(<$generic>)? $type {
pub fn set_header<Name, Value>(mut self, name: Name, value: Value) -> Self
where
Name: TryInto<HeaderName>,
<Name as TryInto<HeaderName>>::Error: Into<http::Error>,
Value: TryInto<HeaderValue>,
<Value as TryInto<HeaderValue>>::Error: Into<http::Error>,
{
self.meta
.change_header(name, value, |name, value| HeaderChange::Set { name, value });
self
}
pub fn add_header<Name, Value>(mut self, name: Name, value: Value) -> Self
where
Name: TryInto<HeaderName>,
<Name as TryInto<HeaderName>>::Error: Into<http::Error>,
Value: TryInto<HeaderValue>,
<Value as TryInto<HeaderValue>>::Error: Into<http::Error>,
{
self.meta
.change_header(name, value, |name, value| HeaderChange::Add { name, value });
self
}
pub fn set_cookie(mut self, cookie: impl Into<Cookie<'static>>) -> Self {
self.meta.set_cookie(cookie.into());
self
}
pub fn headers(&self) -> HeaderMap {
self.meta.headers()
}
pub fn cookies(&self) -> &[Cookie<'static>] {
&self.meta.cookies
}
}
};
}
macro_rules! status_method {
($type:ty $(, $generic:ident)?) => {
impl$(<$generic>)? $type {
pub fn set_status(mut self, status: StatusCode) -> Self {
self.meta.status = Some(status);
self
}
pub fn status(&self) -> Option<StatusCode> {
self.meta.status
}
}
};
}
macro_rules! document_methods {
($type:ty $(, $generic:ident)?) => {
impl$(<$generic>)? $type {
pub fn set_html_attr(mut self, name: impl Into<String>, value: impl Into<AttrValue>) -> Self {
self.document.html_attrs.push((name.into(), value.into()));
self
}
pub fn set_head_attr(mut self, name: impl Into<String>, value: impl Into<AttrValue>) -> Self {
self.document.head_attrs.push((name.into(), value.into()));
self
}
pub fn set_body_attr(mut self, name: impl Into<String>, value: impl Into<AttrValue>) -> Self {
self.document.body_attrs.push((name.into(), value.into()));
self
}
pub fn add_head_el(mut self, el: HeadEl) -> Self {
self.document.head_items.push(HeadItem::El(el));
self
}
pub fn remove_head_el(mut self, el: HeadEl) -> Self {
self.document.head_items.push(HeadItem::Remove(el));
self
}
pub fn add_head_dangerous_raw_html(mut self, html: impl Into<String>) -> Self {
self.document.head_items.push(HeadItem::DangerousRawHtml(html.into()));
self
}
pub fn html_attrs(&self) -> &[(String, AttrValue)] {
&self.document.html_attrs
}
pub fn head_attrs(&self) -> &[(String, AttrValue)] {
&self.document.head_attrs
}
pub fn body_attrs(&self) -> &[(String, AttrValue)] {
&self.document.body_attrs
}
pub fn head_items(&self) -> &[HeadItem] {
&self.document.head_items
}
}
};
}
#[derive(Debug)]
#[must_use]
pub struct Render<Output> {
pub(crate) output: Output,
pub(crate) document: DocumentParts,
pub(crate) meta: ResponseMeta,
}
impl<Output> Render<Output> {
pub fn new(output: Output) -> Self {
Self {
output,
document: DocumentParts::default(),
meta: ResponseMeta::default(),
}
}
}
impl<Output> Render<Output> {
pub fn output(&self) -> &Output {
&self.output
}
}
impl<Output> From<Output> for Render<Output> {
fn from(output: Output) -> Self {
Self::new(output)
}
}
status_method!(Render<Output>, Output);
response_meta_methods!(Render<Output>, Output);
document_methods!(Render<Output>, Output);
#[derive(Debug)]
#[must_use]
pub struct Respond<Output> {
pub(crate) output: Output,
pub(crate) meta: ResponseMeta,
}
impl<Output> Respond<Output> {
pub fn new(output: Output) -> Self {
Self {
output,
meta: ResponseMeta::default(),
}
}
}
impl<Output> Respond<Output> {
pub fn output(&self) -> &Output {
&self.output
}
}
status_method!(Respond<Output>, Output);
response_meta_methods!(Respond<Output>, Output);
#[derive(Debug, Default)]
#[must_use]
pub struct Continue {
pub(crate) document: DocumentParts,
pub(crate) meta: ResponseMeta,
}
impl Continue {
pub fn new() -> Self {
Self::default()
}
}
impl From<()> for Continue {
fn from((): ()) -> Self {
Self::new()
}
}
status_method!(Continue);
response_meta_methods!(Continue);
document_methods!(Continue);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RedirectStatus {
Found,
SeeOther,
TemporaryRedirect,
PermanentRedirect,
}
impl RedirectStatus {
pub(crate) fn to_status_code(self) -> StatusCode {
match self {
Self::Found => StatusCode::FOUND,
Self::SeeOther => StatusCode::SEE_OTHER,
Self::TemporaryRedirect => StatusCode::TEMPORARY_REDIRECT,
Self::PermanentRedirect => StatusCode::PERMANENT_REDIRECT,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[must_use]
pub struct PageTarget {
pub(crate) pattern: &'static str,
pub(crate) params: Vec<(String, String)>,
pub(crate) splat_values: Vec<String>,
pub(crate) input: Option<Result<serde_json::Value, String>>,
pub(crate) hash: Option<String>,
}
impl PageTarget {
pub fn new<Params: ViewPatternParams>(params: Params) -> Self {
let decoded_match = params.into_decoded_match();
Self {
pattern: Params::PATTERN,
params: decoded_match.params,
splat_values: decoded_match.splat_values,
input: None,
hash: None,
}
}
pub fn set_input<Input: Serialize>(mut self, input: &Input) -> Self {
self.input = Some(serde_json::to_value(input).map_err(|error| error.to_string()));
self
}
pub fn set_hash(mut self, hash: impl Into<String>) -> Self {
self.hash = Some(hash.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[must_use]
pub struct ApiEndpointTarget {
pub(crate) pattern: &'static str,
pub(crate) params: Vec<(String, String)>,
pub(crate) splat_values: Vec<String>,
pub(crate) input: Option<Result<serde_json::Value, String>>,
}
impl ApiEndpointTarget {
pub fn new<Params: ApiPatternParams>(params: Params) -> Self {
let decoded_match = params.into_decoded_match();
Self {
pattern: Params::PATTERN,
params: decoded_match.params,
splat_values: decoded_match.splat_values,
input: None,
}
}
pub fn set_input<Input: Serialize>(mut self, input: &Input) -> Self {
self.input = Some(serde_json::to_value(input).map_err(|error| error.to_string()));
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum RedirectTarget {
Url(String),
Page(PageTarget),
}
impl From<PageTarget> for RedirectTarget {
fn from(target: PageTarget) -> Self {
Self::Page(target)
}
}
#[derive(Debug)]
#[must_use]
pub struct Redirect {
pub(crate) target: RedirectTarget,
pub(crate) status: RedirectStatus,
pub(crate) meta: ResponseMeta,
}
impl Redirect {
pub fn new(target: impl Into<RedirectTarget>, status: RedirectStatus) -> Self {
Self {
target: target.into(),
status,
meta: ResponseMeta::default(),
}
}
}
impl Redirect {
pub fn target(&self) -> &RedirectTarget {
&self.target
}
pub fn status(&self) -> RedirectStatus {
self.status
}
}
response_meta_methods!(Redirect);
#[derive(Debug)]
#[must_use]
pub struct ApiEndpointRedirect {
pub(crate) target: ApiEndpointTarget,
pub(crate) status: RedirectStatus,
pub(crate) meta: ResponseMeta,
}
impl ApiEndpointRedirect {
pub fn new(target: ApiEndpointTarget, status: RedirectStatus) -> Self {
Self {
target,
status,
meta: ResponseMeta::default(),
}
}
}
impl ApiEndpointRedirect {
pub fn target(&self) -> &ApiEndpointTarget {
&self.target
}
pub fn status(&self) -> RedirectStatus {
self.status
}
}
response_meta_methods!(ApiEndpointRedirect);
#[derive(Debug, Default)]
#[must_use]
pub struct NotModified {
pub(crate) meta: ResponseMeta,
}
impl NotModified {
pub fn new() -> Self {
Self::default()
}
}
response_meta_methods!(NotModified);
#[derive(Debug)]
pub enum ApiEndpointResult<Output> {
Respond(Respond<Output>),
Redirect(ApiEndpointRedirect),
NotModified(NotModified),
}
impl<Output: Serialize> From<Output> for ApiEndpointResult<Output> {
fn from(output: Output) -> Self {
Self::Respond(Respond::new(output))
}
}
impl<Output> From<Respond<Output>> for ApiEndpointResult<Output> {
fn from(respond: Respond<Output>) -> Self {
Self::Respond(respond)
}
}
impl<Output> From<ApiEndpointRedirect> for ApiEndpointResult<Output> {
fn from(redirect: ApiEndpointRedirect) -> Self {
Self::Redirect(redirect)
}
}
impl<Output> From<NotModified> for ApiEndpointResult<Output> {
fn from(not_modified: NotModified) -> Self {
Self::NotModified(not_modified)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum NoDeclaredError {}
impl TsGen for NoDeclaredError {
fn type_ref() -> TypeRef {
TypeRef::Union(Vec::new())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestRejected {
pub client_msg: String,
}
impl Serialize for RequestRejected {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("RequestRejected", 1)?;
state.serialize_field("client_msg", &self.client_msg)?;
state.end()
}
}
const REQUEST_REJECTED_TYPE_KEY: &str = "vorma::RequestRejected";
impl TsGen for RequestRejected {
fn type_ref() -> TypeRef {
TypeRef::named_with_key(REQUEST_REJECTED_TYPE_KEY, "RequestRejected")
}
fn collect_type_defs(registry: &mut TypeRegistry) -> primitives_rs::tsgen::Result<()> {
registry.try_define(TypeDef::new(
REQUEST_REJECTED_TYPE_KEY,
"RequestRejected",
TypeRef::Object(vec![FieldDef::new(
"client_msg",
TypeRef::String,
FieldPresence::Required,
)]),
))?;
Ok(())
}
}
#[must_use]
pub struct Failure<DeclaredError> {
pub(crate) kind: FailureKind<DeclaredError>,
pub(crate) status: StatusCode,
pub(crate) document: DocumentParts,
pub(crate) meta: ResponseMeta,
}
pub(crate) enum FailureKind<DeclaredError> {
Declared(DeclaredError),
Undeclared(Box<dyn StdError + Send + Sync>),
Invalid(String),
}
impl<DeclaredError> Failure<DeclaredError> {
fn new(kind: FailureKind<DeclaredError>, status: StatusCode) -> Self {
Self {
kind,
status,
document: DocumentParts::default(),
meta: ResponseMeta::default(),
}
}
pub fn from_declared(error: DeclaredError, status: StatusCode) -> Self {
Self::new(FailureKind::Declared(error), status)
}
pub fn from_undeclared(
error: impl Into<Box<dyn StdError + Send + Sync>>,
status: StatusCode,
) -> Self {
Self::new(FailureKind::Undeclared(error.into()), status)
}
}
impl<DeclaredError> Failure<DeclaredError> {
pub fn status(&self) -> StatusCode {
self.status
}
pub fn declared_error(&self) -> Option<&DeclaredError> {
match &self.kind {
FailureKind::Declared(error) => Some(error),
FailureKind::Undeclared(_) | FailureKind::Invalid(_) => None,
}
}
pub fn undeclared_error(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
match &self.kind {
FailureKind::Undeclared(error) => Some(error.as_ref()),
FailureKind::Declared(_) | FailureKind::Invalid(_) => None,
}
}
}
response_meta_methods!(Failure<DeclaredError>, DeclaredError);
document_methods!(Failure<DeclaredError>, DeclaredError);
impl<DeclaredError> fmt::Debug for Failure<DeclaredError> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match &self.kind {
FailureKind::Declared(_) => "declared",
FailureKind::Undeclared(_) => "undeclared",
FailureKind::Invalid(_) => "invalid",
};
formatter
.debug_struct("Failure")
.field("kind", &kind)
.field("status", &self.status)
.field("meta", &self.meta)
.finish()
}
}
#[derive(Debug)]
#[must_use]
pub enum TerminalOutcome<DeclaredError> {
Redirect(Box<Redirect>),
Failure(Box<Failure<DeclaredError>>),
}
impl<DeclaredError> From<Redirect> for TerminalOutcome<DeclaredError> {
fn from(redirect: Redirect) -> Self {
Self::Redirect(Box::new(redirect))
}
}
impl<DeclaredError> From<Failure<DeclaredError>> for TerminalOutcome<DeclaredError> {
fn from(failure: Failure<DeclaredError>) -> Self {
Self::Failure(Box::new(failure))
}
}
impl<DeclaredError: 'static, Error> From<Error> for TerminalOutcome<DeclaredError>
where
Error: StdError + Send + Sync + 'static,
{
fn from(error: Error) -> Self {
if TypeId::of::<Error>() == TypeId::of::<DeclaredError>() {
let name = type_name::<Error>();
return Self::from(Failure::new(
FailureKind::Invalid(format!(
"`?` converted a value of the declared error type {name}; return \
`Failure::from_declared(error, status)` instead"
)),
StatusCode::INTERNAL_SERVER_ERROR,
));
}
Self::from(Failure::from_undeclared(
error,
StatusCode::INTERNAL_SERVER_ERROR,
))
}
}