use crate::future::{Route, RouteFuture};
use alloc::boxed::Box;
use crate::job_id_router::JobIdRouter;
use crate::util::try_downcast;
use blueprint_core::{IntoJobResult, Job, JobCall, JobId, JobResult};
use alloc::sync::Arc;
use alloc::vec::Vec;
use bytes::Bytes;
use core::marker::PhantomData;
use core::pin::Pin;
use core::task::{Context, Poll};
use core::{fmt, iter};
use futures::StreamExt;
use futures::stream::FuturesUnordered;
use tower::{BoxError, Layer, Service};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct RouteId(pub u32);
#[must_use]
pub struct Router<Ctx = ()> {
inner: Arc<JobIdRouter<Ctx>>,
}
impl<Ctx> Clone for Router<Ctx> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<Ctx> Default for Router<Ctx>
where
Ctx: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<Ctx> fmt::Debug for Router<Ctx> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Router")
.field("inner", &self.inner)
.finish()
}
}
impl<Ctx> Router<Ctx>
where
Ctx: Clone + Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: Arc::new(JobIdRouter::default()),
}
}
fn into_inner(self) -> JobIdRouter<Ctx> {
Arc::try_unwrap(self.inner).unwrap_or_else(|arc| arc.as_ref().clone())
}
#[track_caller]
pub fn route<I, J, T>(self, job_id: I, job: J) -> Self
where
I: Into<JobId>,
J: Job<T, Ctx>,
T: 'static,
{
let mut inner = self.into_inner();
inner.route(job_id, job);
Router {
inner: Arc::new(inner),
}
}
pub fn route_service<T>(self, job_id: u32, service: T) -> Self
where
T: Service<JobCall, Error = BoxError> + Clone + Send + Sync + 'static,
T::Response: IntoJobResult,
T::Future: Send + 'static,
{
let service = match try_downcast::<Router<Ctx>, _>(service) {
Ok(_) => {
panic!("Invalid route: `Router::route_service` cannot be used with `Router`s.");
}
Err(service) => service,
};
let mut inner = self.into_inner();
inner.route_service(job_id, service);
Router {
inner: Arc::new(inner),
}
}
#[track_caller]
pub fn always<J, T>(self, job: J) -> Self
where
J: Job<T, Ctx>,
T: 'static,
{
let mut inner = self.into_inner();
inner.always(job);
Router {
inner: Arc::new(inner),
}
}
#[track_caller]
pub fn fallback<J, T>(self, job: J) -> Self
where
J: Job<T, Ctx>,
T: 'static,
{
let mut inner = self.into_inner();
inner.fallback(job);
Router {
inner: Arc::new(inner),
}
}
pub fn layer<L>(self, layer: L) -> Router<Ctx>
where
L: Layer<Route> + Clone + Send + Sync + 'static,
L::Service: Service<JobCall> + Clone + Send + Sync + 'static,
<L::Service as Service<JobCall>>::Response: IntoJobResult + 'static,
<L::Service as Service<JobCall>>::Error: Into<BoxError> + 'static,
<L::Service as Service<JobCall>>::Future: Send + 'static,
{
let inner = self.into_inner().layer(layer);
Router {
inner: Arc::new(inner),
}
}
#[must_use]
pub fn has_routes(&self) -> bool {
self.inner.has_routes()
}
#[doc = include_str!("../docs/with_context.md")]
pub fn with_context<Ctx2>(self, context: Ctx) -> Router<Ctx2> {
let inner = self.into_inner().with_context(context);
Router {
inner: Arc::new(inner),
}
}
pub(crate) fn call_with_context(
&self,
call: JobCall,
context: Ctx,
) -> Option<FuturesUnordered<RouteFuture<BoxError>>> {
blueprint_core::trace!(
target: "blueprint-router",
job_id = %call.job_id(),
metadata = ?call.metadata(),
body = ?call.body(),
"routing a job call to inner routers"
);
let (call, context) = match self.inner.call_with_context(call, context) {
Ok(matched_call_future) => {
blueprint_core::trace!(
target: "blueprint-router",
matched_calls = matched_call_future.len(),
"A route matched this job call"
);
return Some(matched_call_future);
}
Err((call, context)) => (call, context),
};
blueprint_core::trace!(
target: "blueprint-router",
?call,
"No explicit or always route caught this job call, passing to fallback"
);
self.inner
.call_fallback(call, context)
.map(|future| iter::once(future).collect::<FuturesUnordered<_>>())
}
pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, Ctx> {
RouterAsService {
router: self,
_marker: PhantomData,
}
}
}
impl<B> Service<JobCall<B>> for Router<()>
where
B: Into<Bytes>,
{
type Response = Option<Vec<JobResult>>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
#[inline]
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
#[inline]
#[allow(clippy::needless_continue)]
fn call(&mut self, call: JobCall<B>) -> Self::Future {
let Some(mut futures) = self.call_with_context(call.map(Into::into), ()) else {
return Box::pin(async { Ok(None) });
};
Box::pin(async move {
let mut results = Vec::with_capacity(futures.len());
while let Some(item) = futures.next().await {
blueprint_core::trace!(target: "blueprint-router", outcome = ?item, "Job finished with outcome");
match item {
Ok(Some(job)) => results.push(job),
Ok(None) => continue,
Err(e) => {
blueprint_core::error!(?e, "Job failed");
return Err(e);
}
}
}
Ok(Some(results))
})
}
}
pub struct RouterAsService<'a, B, Ctx = ()> {
router: &'a mut Router<Ctx>,
_marker: PhantomData<B>,
}
impl<B> Service<JobCall<B>> for RouterAsService<'_, B, ()>
where
B: Into<Bytes>,
{
type Response = Option<Vec<JobResult>>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
<Router as Service<JobCall<B>>>::poll_ready(self.router, cx)
}
#[inline]
fn call(&mut self, call: JobCall<B>) -> Self::Future {
self.router.call(call)
}
}
impl<B, Ctx> fmt::Debug for RouterAsService<'_, B, Ctx>
where
Ctx: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RouterAsService")
.field("router", &self.router)
.finish()
}
}
#[test]
fn traits() {
use crate::test_helpers::*;
assert_send::<Router<()>>();
assert_sync::<Router<()>>();
}