use std::{hash::Hash, pin::Pin};
use serde::{Deserialize, Serialize};
use topcoat_core::{context::Cx, error::Result};
use topcoat_router::{
Body, Method, Methods, Path, PathBuf, Route, RouteFuture, RouteId, RouterBuilder,
response::Response,
};
use crate::{Surrogate, Surrogated};
const PROCEDURE_ROUTE_PREFIX: &str = "/_topcoat/procedures";
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProcedureId(&'static str);
impl ProcedureId {
#[must_use]
pub const fn new(inner: &'static str) -> Self {
Self(inner)
}
#[must_use]
fn as_str(&self) -> &str {
self.0
}
}
pub type ProcedureFuture<'cx> = Pin<Box<dyn Future<Output = Result<Response>> + Send + 'cx>>;
pub trait Procedure: Send + Sync + 'static {
fn id(&self) -> ProcedureId;
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> ProcedureFuture<'cx>;
}
impl<P: Procedure + ?Sized> Procedure for &'static P {
fn id(&self) -> ProcedureId {
(**self).id()
}
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> ProcedureFuture<'cx> {
(**self).handle(cx, body)
}
}
#[cfg(feature = "discover")]
inventory::collect!(&'static dyn Procedure);
pub trait TypedProcedure: Procedure {
type Args: Surrogated;
type Output: Surrogated;
}
pub struct ProcedureRoute {
id: RouteId,
path: PathBuf,
procedure: Box<dyn Procedure>,
}
impl ProcedureRoute {
pub fn new(procedure: impl Procedure) -> Self {
Self {
id: RouteId::new(),
path: Path::new(&format!(
"{PROCEDURE_ROUTE_PREFIX}/{}",
procedure.id().as_str()
))
.to_owned(),
procedure: Box::new(procedure),
}
}
}
impl Route for ProcedureRoute {
fn id(&self) -> RouteId {
self.id
}
fn methods(&self) -> Methods<'_> {
Methods::Only(&[Method::POST])
}
fn path(&self) -> &Path {
&self.path
}
fn handle<'cx>(&'cx self, cx: &'cx Cx, body: Body) -> RouteFuture<'cx> {
self.procedure.handle(cx, body)
}
}
pub trait RouterBuilderProcedureExt {
#[must_use]
fn procedure(self, procedure: impl Procedure) -> Self;
#[cfg(feature = "discover")]
#[must_use]
fn discover_procedures(self) -> Self;
}
impl RouterBuilderProcedureExt for RouterBuilder {
fn procedure(self, procedure: impl Procedure) -> Self {
self.route(ProcedureRoute::new(procedure))
}
#[cfg(feature = "discover")]
fn discover_procedures(mut self) -> Self {
for &procedure in inventory::iter::<&'static dyn Procedure>() {
self = self.procedure(procedure);
}
self
}
}
pub struct ProcedureSurrogate<P>(P);
impl<P: TypedProcedure> ProcedureSurrogate<P> {
#[must_use]
pub const fn new(procedure: P) -> Self {
Self(procedure)
}
#[allow(clippy::unused_async)]
pub async fn call(
&self,
_args: <P::Args as Surrogated>::Surrogate,
) -> <P::Output as Surrogated>::Surrogate {
panic!("procedures cannot be executed on the server");
}
}
impl<P> Surrogate for &'static ProcedureSurrogate<P>
where
P: TypedProcedure + Copy + Surrogated<Surrogate = Self>,
{
type Real = P;
fn into_real(self) -> Self::Real {
self.0
}
}
impl<P: TypedProcedure> Serialize for ProcedureSurrogate<P> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct TaggedProcedure {
t: &'static str,
id: ProcedureId,
}
TaggedProcedure {
t: "Procedure",
id: self.0.id(),
}
.serialize(serializer)
}
}