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};
use crate::layer::graph::{LayerDiagnostic, LayerGraph, LayerNode};
type LayerBuildFn<E> =
dyn Fn(ServiceContext) -> BoxFuture<'static, Result<ServiceContext, E>> + 'static;
pub trait LayerRequirements {
fn layer_requirements() -> Vec<&'static str>;
}
impl LayerRequirements for () {
fn layer_requirements() -> Vec<&'static str> {
Vec::new()
}
}
impl<S: Service> LayerRequirements for S {
fn layer_requirements() -> Vec<&'static str> {
vec![S::NAME]
}
}
impl<A: Service, B: Service> LayerRequirements for (A, B) {
fn layer_requirements() -> Vec<&'static str> {
vec![A::NAME, B::NAME]
}
}
impl<A: Service, B: Service, C: Service> LayerRequirements for (A, B, C) {
fn layer_requirements() -> Vec<&'static str> {
vec![A::NAME, B::NAME, C::NAME]
}
}
pub struct Layer<ROut, E, RIn = ()>
where
ROut: 'static,
E: 'static,
RIn: 'static,
{
name: &'static str,
build: Rc<LayerBuildFn<E>>,
nodes: Vec<LayerNode>,
_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),
nodes: self.nodes.clone(),
_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),
nodes: Vec::new(),
_pd: PhantomData,
}
}
#[inline]
fn from_context_with_nodes<F>(name: &'static str, build: F, nodes: Vec<LayerNode>) -> Self
where
F: Fn(ServiceContext) -> BoxFuture<'static, Result<ServiceContext, E>> + 'static,
{
Self {
name,
build: Rc::new(build),
nodes,
_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;
let nodes = self.nodes.clone();
Layer::from_context_with_nodes(
name,
move |context| {
let layer = self.clone();
let f = f.clone();
box_future(async move { layer.build_with(context).await.map_err(f) })
},
nodes,
)
}
#[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;
let nodes = self.nodes.clone();
Self::from_context_with_nodes(
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
})
},
nodes,
)
}
#[inline]
pub fn merge<ROut2>(self, that: Layer<ROut2, E, RIn>) -> Layer<(ROut, ROut2), E, RIn>
where
ROut2: 'static,
{
let mut nodes = self.nodes.clone();
nodes.extend(that.nodes.clone());
Layer::from_context_with_nodes(
"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))
})
},
nodes,
)
}
#[inline]
pub fn provide<RProvider>(self, provider: Layer<RProvider, E, ()>) -> Layer<ROut, E, ()>
where
RProvider: 'static,
{
let mut nodes = self.nodes.clone();
nodes.extend(provider.nodes.clone());
Layer::from_context_with_nodes(
"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
})
},
nodes,
)
}
#[inline]
pub fn provide_merge<RProvider>(
self,
provider: Layer<RProvider, E, ()>,
) -> Layer<(ROut, RProvider), E, ()>
where
RProvider: 'static,
{
let mut nodes = self.nodes.clone();
nodes.extend(provider.nodes.clone());
Layer::from_context_with_nodes(
"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))
})
},
nodes,
)
}
#[inline]
pub fn diagnostics(&self) -> Vec<LayerDiagnostic> {
self.diagnostics_with_context(&ServiceContext::empty())
}
#[inline]
pub fn diagnostics_with_context(&self, context: &ServiceContext) -> Vec<LayerDiagnostic> {
let mut nodes = self.nodes.clone();
let already_provided: std::collections::HashSet<&str> = self
.nodes
.iter()
.flat_map(|n| n.provides.iter().map(|s| s.as_str()))
.collect();
for name in context.service_names() {
if !already_provided.contains(name) {
nodes.push(LayerNode::new(name, Vec::<&str>::new(), [name]));
}
}
LayerGraph::new(nodes).diagnostics()
}
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 {
let nodes = vec![LayerNode::new(S::NAME, Vec::<&str>::new(), [S::NAME])];
Self::from_context_with_nodes(
S::NAME,
move |_context| {
let service = service.clone();
box_future(async move { Ok(ServiceContext::empty().add(service)) })
},
nodes,
)
}
}
impl<S, E, RIn> Layer<S, E, RIn>
where
S: Service,
E: 'static,
RIn: LayerRequirements + 'static,
{
#[inline]
pub fn effect<F>(name: &'static str, make: F) -> Self
where
F: Fn() -> Effect<S, E, ServiceContext> + 'static,
{
let nodes = vec![LayerNode::new(name, RIn::layer_requirements(), [S::NAME])];
Self::from_context_with_nodes(
name,
move |mut context| {
let effect = make();
box_future(async move {
let service = effect.run(&mut context).await?;
Ok(ServiceContext::empty().add(service))
})
},
nodes,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContextService, MissingService, run_blocking};
#[derive(Clone, Debug, PartialEq, effectful::Service)]
struct Config {
url: String,
}
#[derive(Clone, Debug, PartialEq, effectful::Service)]
struct Database {
url: String,
}
#[derive(Clone, Debug, PartialEq, effectful::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
));
}
mod diagnostics {
use super::*;
#[test]
fn diagnostics_when_effect_layer_missing_provider_returns_stable_service_key() {
let database = Layer::<Database, MissingService, Config>::effect("Database", || {
Config::use_sync(|config| Database { url: config.url })
});
let diagnostics = database.diagnostics();
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "missing-provider");
assert!(diagnostics[0].message.contains(Config::NAME));
}
#[test]
fn diagnostics_when_provider_supplies_requirement_returns_empty() {
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 diagnostics = database.provide(config).diagnostics();
assert!(diagnostics.is_empty());
}
#[test]
fn diagnostics_with_context_when_context_supplies_requirement_returns_empty() {
let database = Layer::<Database, MissingService, Config>::effect("Database", || {
Config::use_sync(|config| Database { url: config.url })
});
let ctx = ServiceContext::empty().add(Config {
url: "postgres://".to_string(),
});
let diagnostics = database.diagnostics_with_context(&ctx);
assert!(diagnostics.is_empty());
}
#[test]
fn diagnostics_merge_preserves_existing_build_behavior() {
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 diagnostics_provide_preserves_existing_build_behavior() {
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 diagnostics_provide_merge_preserves_existing_build_behavior() {
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 merged = database.provide_merge(config);
let diagnostics = merged.diagnostics();
assert!(diagnostics.is_empty());
}
#[test]
fn diagnostics_service_layer_preserves_existing_build_behavior() {
let config = Config {
url: "postgres://".to_string(),
};
let layer = config.layer();
let context = run_blocking(layer.build(), ()).expect("layer should build");
assert!(context.contains::<Config>());
}
#[test]
fn diagnostics_succeed_preserves_existing_build_behavior() {
let layer = Layer::<Config, MissingService>::succeed(Config {
url: "postgres://".to_string(),
});
let context = run_blocking(layer.build(), ()).expect("layer should build");
assert!(context.contains::<Config>());
}
#[test]
fn diagnostics_with_context_when_service_already_provided_by_layer_returns_empty() {
let layer = Layer::<Config, MissingService>::succeed(Config {
url: "postgres://".to_string(),
});
let ctx = ServiceContext::empty().add(Config {
url: "other://".to_string(),
});
let diagnostics = layer.diagnostics_with_context(&ctx);
assert!(diagnostics.is_empty());
}
}
}