use std::cell::RefCell;
use std::fmt;
use std::marker::PhantomData;
use std::rc::Rc;
use crate::context::{Service, ServiceContext};
use crate::kernel::{BoxFuture, Effect, box_future};
type LayerBuildFn<E> =
dyn Fn(ServiceContext) -> BoxFuture<'static, Result<ServiceContext, E>> + 'static;
pub struct Layer<ROut, E, RIn = ()>
where
ROut: 'static,
E: 'static,
RIn: 'static,
{
name: &'static str,
build: Rc<LayerBuildFn<E>>,
_pd: PhantomData<fn(RIn) -> ROut>,
}
impl<ROut, E, RIn> Clone for Layer<ROut, E, RIn>
where
ROut: 'static,
E: 'static,
RIn: 'static,
{
fn clone(&self) -> Self {
Self {
name: self.name,
build: Rc::clone(&self.build),
_pd: PhantomData,
}
}
}
impl<ROut, E, RIn> fmt::Debug for Layer<ROut, E, RIn>
where
ROut: 'static,
E: 'static,
RIn: 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Layer").field("name", &self.name).finish()
}
}
impl<ROut, E, RIn> Layer<ROut, E, RIn>
where
ROut: 'static,
E: 'static,
RIn: 'static,
{
#[inline]
pub fn from_context<F>(name: &'static str, build: F) -> Self
where
F: Fn(ServiceContext) -> BoxFuture<'static, Result<ServiceContext, E>> + 'static,
{
Self {
name,
build: Rc::new(build),
_pd: PhantomData,
}
}
#[inline]
pub fn name(&self) -> &'static str {
self.name
}
#[inline]
pub fn build_from(&self, context: ServiceContext) -> Effect<ServiceContext, E, ()> {
let layer = self.clone();
Effect::new_async(move |_env: &mut ()| layer.build_with(context))
}
#[inline]
pub fn build(&self) -> Effect<ServiceContext, E, ()> {
self.build_from(ServiceContext::empty())
}
#[inline]
pub fn to_effect(&self) -> Effect<ServiceContext, E, ServiceContext> {
let layer = self.clone();
Effect::new_async(move |context: &mut ServiceContext| layer.build_with(context.clone()))
}
#[inline]
pub fn map_error<E2, F>(self, f: F) -> Layer<ROut, E2, RIn>
where
E2: 'static,
F: Fn(E) -> E2 + Clone + 'static,
{
let name = self.name;
Layer::from_context(name, move |context| {
let layer = self.clone();
let f = f.clone();
box_future(async move { layer.build_with(context).await.map_err(f) })
})
}
#[inline]
pub fn memoized(self) -> Self
where
E: Clone,
{
let cache: Rc<RefCell<Option<Result<ServiceContext, E>>>> = Rc::new(RefCell::new(None));
let name = self.name;
Self::from_context(name, move |context| {
let layer = self.clone();
let cache = Rc::clone(&cache);
if let Some(cached) = cache.borrow().clone() {
return box_future(async move { cached });
}
box_future(async move {
let result = layer.build_with(context).await;
*cache.borrow_mut() = Some(result.clone());
result
})
})
}
#[inline]
pub fn merge<ROut2>(self, that: Layer<ROut2, E, RIn>) -> Layer<(ROut, ROut2), E, RIn>
where
ROut2: 'static,
{
Layer::from_context("Layer.merge", move |context| {
let left = self.clone();
let right = that.clone();
box_future(async move {
let left_context = left.build_with(context.clone()).await?;
let right_context = right.build_with(context).await?;
Ok(left_context.merge(right_context))
})
})
}
#[inline]
pub fn provide<RProvider>(
self,
provider: Layer<RProvider, E, ()>,
) -> Layer<ROut, E, ()>
where
RProvider: 'static,
{
Layer::from_context("Layer.provide", move |context| {
let layer = self.clone();
let provider = provider.clone();
box_future(async move {
let provided = provider.build_with(context.clone()).await?;
let full_context = context.merge(provided);
layer.build_with(full_context).await
})
})
}
#[inline]
pub fn provide_merge<RProvider>(
self,
provider: Layer<RProvider, E, ()>,
) -> Layer<(ROut, RProvider), E, ()>
where
RProvider: 'static,
{
Layer::from_context("Layer.provide_merge", move |context| {
let layer = self.clone();
let provider = provider.clone();
box_future(async move {
let provided = provider.build_with(context.clone()).await?;
let output = layer.build_with(context.merge(provided.clone())).await?;
Ok(provided.merge(output))
})
})
}
pub(crate) fn build_with(
&self,
context: ServiceContext,
) -> BoxFuture<'static, Result<ServiceContext, E>> {
(self.build)(context)
}
}
impl<S, E> Layer<S, E, ()>
where
S: Service,
E: 'static,
{
#[inline]
pub fn succeed(service: S) -> Self {
Self::from_context(S::NAME, move |_context| {
let service = service.clone();
box_future(async move { Ok(ServiceContext::empty().add(service)) })
})
}
}
impl<S, E, RIn> Layer<S, E, RIn>
where
S: Service,
E: 'static,
RIn: 'static,
{
#[inline]
pub fn effect<F>(name: &'static str, make: F) -> Self
where
F: Fn() -> Effect<S, E, ServiceContext> + 'static,
{
Self::from_context(name, move |mut context| {
let effect = make();
box_future(async move {
let service = effect.run(&mut context).await?;
Ok(ServiceContext::empty().add(service))
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MissingService, Service, run_blocking};
#[derive(Clone, Debug, PartialEq, Service)]
struct Config {
url: String,
}
#[derive(Clone, Debug, PartialEq, Service)]
struct Database {
url: String,
}
#[derive(Clone, Debug, PartialEq, Service)]
struct Logger {
level: String,
}
#[test]
fn succeed_builds_service_context() {
let layer = Layer::<Config, MissingService>::succeed(Config {
url: "postgres://".to_string(),
});
let context = run_blocking(layer.build(), ()).expect("layer should build");
assert_eq!(
context.get::<Config>(),
Some(&Config {
url: "postgres://".to_string()
})
);
}
#[test]
fn provide_satisfies_layer_dependencies() {
let config = Layer::<Config, MissingService>::succeed(Config {
url: "postgres://".to_string(),
});
let database =
Layer::<Database, MissingService, Config>::effect("Database", || {
Config::use_sync(|config| Database { url: config.url })
});
let program: Effect<String, MissingService, ServiceContext> =
Database::use_sync(|database| database.url);
let result = run_blocking(program.provide(database.provide(config)), ());
assert_eq!(result, Ok("postgres://".to_string()));
}
#[test]
fn merge_keeps_services_from_both_layers() {
let config = Layer::<Config, MissingService>::succeed(Config {
url: "postgres://".to_string(),
});
let logger = Layer::<Logger, MissingService>::succeed(Logger {
level: "debug".to_string(),
});
let context = run_blocking(config.merge(logger).build(), ()).expect("layer should build");
assert!(context.contains::<Config>());
assert!(context.contains::<Logger>());
}
#[test]
fn missing_dependency_fails_layer_build() {
let database =
Layer::<Database, MissingService, Config>::effect("Database", || {
Config::use_sync(|config| Database { url: config.url })
});
let result = run_blocking(database.build(), ());
assert!(matches!(
result,
Err(MissingService { name }) if name == Config::NAME
));
}
}