New approach to dependency injection for 1.x

This commit is contained in:
Diggory Blake 2023-07-02 00:07:15 +01:00
parent e94a833eaf
commit f8ebe14790
No known key found for this signature in database
GPG key ID: E6BDFA83146ABD40
17 changed files with 840 additions and 822 deletions

86
src/async_.rs Normal file
View file

@ -0,0 +1,86 @@
use std::{
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use crate::{
resource::{unwrap_resource, Resource},
slot::SlotDesc,
state::Aerosol,
};
pub(crate) struct WaitForSlot<T: Resource> {
state: Aerosol,
wait_index: Option<usize>,
insert_placeholder: bool,
phantom: PhantomData<fn() -> T>,
}
impl<T: Resource> Future for WaitForSlot<T> {
type Output = Option<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
this.state
.poll_for_slot(&mut this.wait_index, || cx.waker(), this.insert_placeholder)
}
}
impl Aerosol {
pub(crate) fn wait_for_slot_async<T: Resource>(
&self,
insert_placeholder: bool,
) -> WaitForSlot<T> {
WaitForSlot {
state: self.clone(),
wait_index: None,
insert_placeholder,
phantom: PhantomData,
}
}
/// Tries to get an instance of `T` from the AppState. Returns `None` if there is no such instance.
/// This function does not attempt to construct `T` if it does not exist.
pub async fn try_get_async<T: Resource>(&self) -> Option<T> {
match self.try_get_slot()? {
SlotDesc::Filled(x) => Some(x),
SlotDesc::Placeholder => self.wait_for_slot_async::<T>(false).await,
}
}
/// Get an instance of `T` from the AppState, and panic if not found.
/// This function does not attempt to construct `T` if it does not exist.
pub async fn get_async<T: Resource>(&self) -> T {
unwrap_resource(self.try_get_async().await)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn get_with() {
let state = Aerosol::new().with(42);
assert_eq!(state.get_async::<i32>().await, 42);
}
#[tokio::test]
async fn get_inserted() {
let state = Aerosol::new();
state.insert(42);
assert_eq!(state.get_async::<i32>().await, 42);
}
#[tokio::test]
async fn try_get_some() {
let state = Aerosol::new().with(42);
assert_eq!(state.try_get_async::<i32>().await, Some(42));
}
#[tokio::test]
async fn try_get_none() {
let state = Aerosol::new().with("Hello");
assert_eq!(state.try_get_async::<i32>().await, None);
}
}

189
src/async_constructible.rs Normal file
View file

@ -0,0 +1,189 @@
use std::error::Error;
use async_trait::async_trait;
use crate::{
resource::{unwrap_constructed, Resource},
slot::SlotDesc,
state::Aerosol,
ConstructibleResource,
};
/// Implemented for resources which can be constructed asynchronously from other
/// resources. Requires feature `async`.
#[async_trait]
pub trait AsyncConstructibleResource: Resource {
/// Error type for when resource fails to be constructed.
type Error: Error + Send + Sync;
/// Construct the resource with the provided application state.
async fn construct_async(aero: &Aerosol) -> Result<Self, Self::Error>;
}
#[async_trait]
impl<T: ConstructibleResource> AsyncConstructibleResource for T {
type Error = <T as ConstructibleResource>::Error;
async fn construct_async(aero: &Aerosol) -> Result<Self, Self::Error> {
Self::construct(aero)
}
}
impl Aerosol {
/// Try to get or construct an instance of `T` asynchronously. Requires feature `async`.
pub async fn try_obtain_async<T: AsyncConstructibleResource>(&self) -> Result<T, T::Error> {
match self.try_get_slot() {
Some(SlotDesc::Filled(x)) => Ok(x),
Some(SlotDesc::Placeholder) | None => match self.wait_for_slot_async::<T>(true).await {
Some(x) => Ok(x),
None => match T::construct_async(self).await {
Ok(x) => {
self.fill_placeholder::<T>(x.clone());
Ok(x)
}
Err(e) => {
self.clear_placeholder::<T>();
Err(e)
}
},
},
}
}
/// Get or construct an instance of `T` asynchronously. Panics if unable. Requires feature `async`.
pub async fn obtain_async<T: AsyncConstructibleResource>(&self) -> T {
unwrap_constructed(self.try_obtain_async::<T>().await)
}
}
#[cfg(test)]
mod tests {
use std::{convert::Infallible, time::Duration};
use super::*;
#[derive(Debug, Clone)]
struct Dummy;
#[async_trait]
impl AsyncConstructibleResource for Dummy {
type Error = Infallible;
async fn construct_async(_app_state: &Aerosol) -> Result<Self, Self::Error> {
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(Self)
}
}
#[tokio::test]
async fn obtain() {
let state = Aerosol::new();
state.obtain_async::<Dummy>().await;
}
#[tokio::test]
async fn obtain_race() {
let state = Aerosol::new();
let mut handles = Vec::new();
for _ in 0..100 {
let state = state.clone();
handles.push(tokio::spawn(async move {
state.obtain_async::<Dummy>().await;
}));
}
for handle in handles {
handle.await.unwrap();
}
}
#[derive(Debug, Clone)]
struct DummyRecursive;
#[async_trait]
impl AsyncConstructibleResource for DummyRecursive {
type Error = Infallible;
async fn construct_async(aero: &Aerosol) -> Result<Self, Self::Error> {
aero.obtain_async::<Dummy>().await;
Ok(Self)
}
}
#[tokio::test]
async fn obtain_recursive() {
let state = Aerosol::new();
state.obtain_async::<DummyRecursive>().await;
}
#[tokio::test]
async fn obtain_recursive_race() {
let state = Aerosol::new();
let mut handles = Vec::new();
for _ in 0..100 {
let state = state.clone();
handles.push(tokio::spawn(async move {
state.obtain_async::<DummyRecursive>().await;
}));
}
}
#[derive(Debug, Clone)]
struct DummyCyclic;
#[async_trait]
impl AsyncConstructibleResource for DummyCyclic {
type Error = Infallible;
async fn construct_async(aero: &Aerosol) -> Result<Self, Self::Error> {
aero.obtain_async::<DummyCyclic>().await;
Ok(Self)
}
}
#[tokio::test]
#[should_panic(expected = "Cycle detected")]
async fn obtain_cyclic() {
let state = Aerosol::new();
state.obtain_async::<DummyCyclic>().await;
}
#[derive(Debug, Clone)]
struct DummySync;
impl ConstructibleResource for DummySync {
type Error = Infallible;
fn construct(_app_state: &Aerosol) -> Result<Self, Self::Error> {
std::thread::sleep(Duration::from_millis(100));
Ok(Self)
}
}
#[derive(Debug, Clone)]
struct DummySyncRecursive;
#[async_trait]
impl AsyncConstructibleResource for DummySyncRecursive {
type Error = Infallible;
async fn construct_async(aero: &Aerosol) -> Result<Self, Self::Error> {
aero.obtain_async::<DummySync>().await;
Ok(Self)
}
}
#[tokio::test]
async fn obtain_sync_recursive() {
let state = Aerosol::new();
state.obtain_async::<DummySyncRecursive>().await;
}
#[tokio::test]
async fn obtain_sync_recursive_race() {
let state = Aerosol::new();
let mut handles = Vec::new();
for _ in 0..100 {
let state = state.clone();
handles.push(tokio::spawn(async move {
state.obtain_async::<DummySyncRecursive>().await;
}));
}
}
}

98
src/axum.rs Normal file
View file

@ -0,0 +1,98 @@
//! Integration with the `axum` web framework.
//!
//! Provies the `Dep` and `Obtain` axum extractors for easily accessing
//! resources from within route handlers.
//!
//! To make use of these extractors, your application state must either be
//! an `Aerosol`, or you must implement `FromRef<YourState>` for `Aerosol`.
use std::any::type_name;
use async_trait::async_trait;
use axum::{
extract::{FromRef, FromRequestParts},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
};
use crate::{Aerosol, AsyncConstructibleResource, ConstructibleResource, Resource};
/// Type of axum Rejection returned when a resource cannot be acquired
#[derive(Debug, thiserror::Error)]
pub enum DependencyError {
/// Tried to get a resource which did not exist. Use `Obtain(..)` if you want aerosol to
/// try to construct the resource on demand.
#[error("Resource `{name}` does not exist")]
DoesNotExist {
/// Name of the resource type
name: &'static str,
},
/// Tried and failed to construct a resource.
#[error("Failed to construct `{name}`: {source}")]
FailedToConstruct {
/// Name of the resource type
name: &'static str,
/// Error returned by the resource constructor
#[source]
source: anyhow::Error,
},
}
impl IntoResponse for DependencyError {
fn into_response(self) -> Response {
tracing::error!("{}", self);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
impl DependencyError {
pub(crate) fn does_not_exist<T>() -> Self {
Self::DoesNotExist {
name: type_name::<T>(),
}
}
pub(crate) fn failed_to_construct<T>(error: impl Into<anyhow::Error>) -> Self {
Self::FailedToConstruct {
name: type_name::<T>(),
source: error.into(),
}
}
}
/// Get an already-existing resource from the state. Equivalent to calling `Aerosol::try_get_async`.
pub struct Dep<T: Resource>(pub T);
#[async_trait]
impl<T: ConstructibleResource, S: Send + Sync> FromRequestParts<S> for Dep<T>
where
Aerosol: FromRef<S>,
{
type Rejection = DependencyError;
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Aerosol::from_ref(state)
.try_get_async()
.await
.map(Self)
.ok_or_else(DependencyError::does_not_exist::<T>)
}
}
/// Get a resource from the state, or construct it if it doesn't exist. Equivalent to calling `Aerosol::try_obtain_async`.
pub struct Obtain<T: AsyncConstructibleResource>(pub T);
#[async_trait]
impl<T: AsyncConstructibleResource, S: Send + Sync> FromRequestParts<S> for Obtain<T>
where
Aerosol: FromRef<S>,
{
type Rejection = DependencyError;
async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Aerosol::from_ref(state)
.try_obtain_async()
.await
.map(Self)
.map_err(DependencyError::failed_to_construct::<T>)
}
}

View file

@ -1,277 +0,0 @@
#[doc(hidden)]
#[macro_export]
macro_rules! private_define_context {
{
$caller:tt
input = [{
$name:ident {
$($body:tt)*
}
}]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::private_define_context }]
rest = [{ $($body)* }]
~~> $crate::private_define_context! {
$caller
name = [{ $name }]
}
}
};
{
$caller:tt
name = [{ $name:ident }]
$(auto_field = [{ $auto_field:ident, $auto_t:ty, $factory:ty, ($($f_args:ident,)*) }])*
$(field = [{ $field:ident, $t:ty }])*
} => {
$crate::tt_call::tt_return! {
$caller
result = [{
#[derive(Clone, Debug)]
struct $name {
$($auto_field: $auto_t,)*
$($field: $t,)*
}
impl $name {
fn new($($field: $t,)*) -> Result<Self, anyhow::Error> {
$(
let $auto_field = <$factory as $crate::Factory<_>>::build(($($f_args.clone(),)*))?;
)*
Ok(Self {
$($auto_field,)*
$($field,)*
})
}
}
$(
impl $crate::Provide<$auto_t> for $name {
fn provide(&self) -> $auto_t {
self.$auto_field.clone()
}
}
impl $crate::ProvideWith<$auto_t> for $name {
fn provide_with<E, F: FnOnce($auto_t) -> Result<$auto_t, E>>(&self, f: F) -> Result<Self, E> {
let mut result = self.clone();
result.$auto_field = f(result.$auto_field)?;
Ok(result)
}
}
)*
$(
impl $crate::Provide<$t> for $name {
fn provide(&self) -> $t {
self.$field.clone()
}
}
impl $crate::ProvideWith<$t> for $name {
fn provide_with<E, F: FnOnce($t) -> Result<$t, E>>(&self, f: F) -> Result<Self, E> {
let mut result = self.clone();
result.$field = f(result.$field)?;
Ok(result)
}
}
)*
}]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty [ ($($f_args:ident),*) $factory:ty ], $($rest:tt)* }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
auto_field = [{ $field_name, $t, $factory, ($($f_args,)*) }]
$(field = [{ $($field)* }])*
rest = [{ $($rest)* }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty [ ($($f_args:ident),*) $factory:ty ] }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
auto_field = [{ $field_name, $t, $factory, ($($f_args,)*) }]
$(field = [{ $($field)* }])*
rest = [{ }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty [ $factory:ty ], $($rest:tt)* }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
auto_field = [{ $field_name, $t, $factory, () }]
$(field = [{ $($field)* }])*
rest = [{ $($rest)* }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty [ $factory:ty ] }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
auto_field = [{ $field_name, $t, $factory, () }]
$(field = [{ $($field)* }])*
rest = [{ }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty, $($rest:tt)* }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
$(field = [{ $($field)* }])*
field = [{ $field_name, $t }]
rest = [{ $($rest)* }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ $field_name:ident: $t:ty }]
} => {
$crate::private_define_context! {
$caller
$(auto_field = [{ $($auto_field)* }])*
$(field = [{ $($field)* }])*
field = [{ $field_name, $t }]
rest = [{ }]
}
};
{
$caller:tt
$(auto_field = [{ $($auto_field:tt)* }])*
$(field = [{ $($field:tt)* }])*
rest = [{ }]
} => {
$crate::tt_call::tt_return! {
$caller
$(auto_field = [{ $($auto_field)* }])*
$(field = [{ $($field)* }])*
}
};
}
/// Define a new context. Typically used at the top level of an
/// application to contain the full set of requried dependencies.
///
/// Contexts follow a struct-like syntax, although the names of
/// fields are for the most part unimportant.
///
/// Contexts automatically implement all applicable interfaces.
/// An interface is applicable if all of the dependencies
/// required by that interface are present in the context.
///
/// Dependencies are identified by *type*, not by the field name.
/// Contexts may not contain two fields of the same type. Instead
/// use new-type wrappers to distinguish similar dependencies.
///
/// Types used in a context must implement `Clone + Debug`, and
/// `Clone` should be a cheap operation. For this reason it is usual
/// to wrap dependencies in an `Rc` or `Arc`.
///
/// A constructor function will be automatically implemented
/// for contexts, with one parameter for each dependency, to be
/// provided in the same order as when the context is defined.
///
/// ## Example
///
/// ```
/// use std::sync::Arc;
///
/// #[derive(Debug)]
/// struct Foo;
/// #[derive(Debug)]
/// struct Bar;
///
/// aerosol::define_context!(
/// TestContext {
/// foo: Arc<Foo>,
/// bar: Arc<Bar>,
/// }
/// );
///
/// fn main() {
/// TestContext::new(
/// Arc::new(Foo),
/// Arc::new(Bar),
/// );
/// }
/// ```
///
/// It is also possible to define a factory type to enable
/// dependencies to be automatically created.
///
/// When a factory is specified for a dependency, it will be
/// omitted from the parameter list required by the context's
/// constructor. Instead, the constructor will call the `build`
/// method on the specified factory.
///
/// To conditionally use a factory, or use different factories
/// for the same dependency, define separate contexts, or
/// call the factory manually and pass the result to the
/// context's constructor in the normal way.
///
/// ## Example
///
/// ```
/// use std::sync::Arc;
///
/// #[derive(Debug)]
/// struct Foo;
/// #[derive(Debug)]
/// struct Bar;
///
/// struct FooFactory;
/// impl aerosol::Factory for FooFactory {
/// type Object = Arc<Foo>;
/// fn build(_: ()) -> Result<Arc<Foo>, anyhow::Error> { Ok(Arc::new(Foo)) }
/// }
///
/// aerosol::define_context!(
/// TestContext {
/// foo: Arc<Foo> [FooFactory],
/// bar: Arc<Bar>,
/// }
/// );
///
/// fn main() {
/// TestContext::new(
/// Arc::new(Bar),
/// );
/// }
/// ```
///
///
#[macro_export]
macro_rules! define_context {
($($input:tt)*) => (
$crate::tt_call::tt_call! {
macro = [{ $crate::private_define_context }]
input = [{ $($input)* }]
}
);
}

View file

@ -1,192 +0,0 @@
#[doc(hidden)]
#[macro_export]
macro_rules! generate_trait_def {
{
$caller:tt
name = [{ $name:ident }]
bounds = [{ $($bounds:tt)+ }]
getters = [{ $(
{ $getter:ident $t:ty }
)* }]
} => {
$crate::tt_call::tt_return! {
$caller
trait_def = [{
pub trait $name: $($bounds)+ {
$(fn $getter(&self) -> $t;)*
}
}]
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! generate_trait_impl {
{
$caller:tt
name = [{ $name:ident }]
bounds = [{ $($bounds:tt)+ }]
getters = [{ $(
{ $getter:ident $t:ty }
)* }]
} => {
$crate::tt_call::tt_return! {
$caller
trait_impl = [{
impl<T: $($bounds)+> $name for T {
$(fn $getter(&self) -> $t {
$crate::Provide::<$t>::provide(self)
})*
}
}]
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! private_define_interface {
{
$caller:tt
input = [{ $($input:tt)* }]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::parse_trait_def }]
input = [{ $($input)* }]
~~> $crate::private_define_interface! {
$caller
}
}
};
{
$caller:tt
name = [{ $name:ident }]
body = [{ $(
fn $getter:ident(&self) -> $t:ty;
)* }]
$(bound = [{ $($bound:tt)* }])*
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::join }]
sep = [{ + }]
$(item = [{ $($bound)* }])*
$(item = [{ $crate::Provide<$t> }])*
~~> $crate::private_define_interface! {
$caller
name = [{ $name }]
getters = [{ $(
{ $getter $t }
)* }]
}
}
};
{
$caller:tt
name = [{ $name:ident }]
getters = [{ $($getters:tt)* }]
joined = [{ $($joined:tt)* }]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::generate_trait_def }]
name = [{ $name }]
bounds = [{ $($joined)* }]
getters = [{ $($getters)* }]
~~> $crate::private_define_interface! {
$caller
name = [{ $name }]
getters = [{ $($getters)* }]
bounds = [{ $($joined)* }]
}
}
};
{
$caller:tt
name = [{ $name:ident }]
getters = [{ $($getters:tt)* }]
bounds = [{ $($bounds:tt)* }]
trait_def = [{ $($trait_def:tt)* }]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::generate_trait_impl }]
name = [{ $name }]
bounds = [{ $($bounds)* }]
getters = [{ $($getters)* }]
~~> $crate::private_define_interface! {
$caller
trait_def = [{ $($trait_def)* }]
}
}
};
{
$caller:tt
trait_def = [{ $($trait_def:tt)* }]
trait_impl = [{ $($trait_impl:tt)* }]
} => {
$crate::tt_call::tt_return! {
$caller
result = [{ $($trait_def)* $($trait_impl)* }]
}
};
}
/// Define a new interface. Used at any layer of your application
/// to declare what dependencies are required by that part of the
/// program.
///
/// Interfaces follow a trait-like syntax, except that they may
/// only contain "getter" methods of a particular form. The names
/// of these methods are for the most part unimportant, but the
/// return types are used to identify dependencies required for
/// a context to implement this interface.
///
/// ## Example
///
/// ```
/// use std::sync::Arc;
///
/// #[derive(Debug)]
/// struct Foo;
///
/// aerosol::define_interface!(
/// TestInterface {
/// fn foo(&self) -> Arc<Foo>;
/// }
/// );
/// ```
///
/// Interfaces may also specify super-traits, which can themselves
/// be interfaces. Interfaces do not need to explicitly list
/// dependencies if they are transitively required by one of their
/// super-traits, but repeating a dependency will still only
/// require it to be provided once.
///
/// ## Example
///
/// ```
/// #![recursion_limit="128"]
/// use std::sync::Arc;
///
/// #[derive(Debug)]
/// struct Foo;
///
/// aerosol::define_interface!(
/// FooInterface {
/// fn foo(&self) -> Arc<Foo>;
/// }
/// );
///
/// aerosol::define_interface!(
/// TestInterface: FooInterface + Clone {}
/// );
/// ```
#[macro_export]
macro_rules! define_interface {
($($input:tt)*) => (
$crate::tt_call::tt_call! {
macro = [{ $crate::private_define_interface }]
input = [{ $($input)* }]
}
);
}

View file

@ -1,56 +0,0 @@
#[doc(hidden)]
#[macro_export]
macro_rules! join {
{
$caller:tt
sep = [{ $($sep:tt)* }]
} => {
$crate::tt_call::tt_return! {
$caller
joined = [{ }]
}
};
{
$caller:tt
sep = [{ $($sep:tt)* }]
item = [{ $($first:tt)* }]
} => {
$crate::tt_call::tt_return! {
$caller
joined = [{ $($first)* }]
}
};
{
$caller:tt
sep = [{ $($sep:tt)* }]
item = [{ $($first:tt)* }]
item = [{ $($second:tt)* }]
$(
item = [{ $($rest:tt)* }]
)*
} => {
$crate::join! {
$caller
sep = [{ $($sep)* }]
item = [{ $($first)* $($sep)* $($second)* }]
$(
item = [{ $($rest)* }]
)*
}
};
}
#[test]
fn test_join() {
use tt_call::*;
let s = tt_call! {
macro = [{ join }]
sep = [{ .chars().rev().collect::<String>() + "_" + & }]
item = [{ "first " }]
item = [{ "second ".trim() }]
item = [{ "third " }]
};
assert_eq!(s, " tsrif_dnoces_third ");
}

View file

@ -1,125 +1,35 @@
#![deny(missing_docs)]
//! # aerosol
//! Simple dependency injection for Rust
//!
//! The two main exports of this crate are the `define_context`
//! and `define_interface` macros.
//! Optional features: `async`
//!
//! Contexts are containers for multiple dependencies, allowing
//! them to be passed around as one with relative ease. Interfaces
//! are specialized traits which place constraints on contexts,
//! indicating exactly what dependencies a context must provide.
//! ## `async`
//!
//! Contexts are typically created at the top level of an application,
//! as they specify exactly what concrete versions of all dependencies
//! are going to be used. A single context is created with a precise
//! set of depenencies, and is then threaded through the rest of the
//! application as a generic parameter.
//! Allows resources to be constructed asynchrously, and provies a corresponding
//! `AsyncConstructibleResource` trait.
//!
//! Interfaces are used at every level of an application, as they
//! allow each piece of code to independently specify what dependencies
//! are required. Interfaces can "inherit" the dependencies of other
//! interfaces, with the idea being that this inheritance will form
//! a tree, such that there will be some "root interface" which contains
//! the union of all dependencies required by the whole application.
//! ## `axum`
//!
//! This pattern allows dependencies to be added or removed from any
//! part of the application without having to modify the code at every
//! level, to thread or un-thread the new or old dependencies through.
//!
//! ## Example
//!
//! ```
//! #![recursion_limit="128"]
//! use std::sync::Arc;
//! use std::fmt::Debug;
//!
//! // We will depend on some kind of logger
//! trait Logger: Debug {
//! fn log(&self, msg: &str);
//! }
//!
//! // We have a specific implementation of a stdout logger
//! #[derive(Debug)]
//! struct StdoutLogger;
//!
//! impl Logger for StdoutLogger {
//! fn log(&self, msg: &str) {
//! println!("{}", msg);
//! }
//! }
//!
//! struct StdoutLoggerFactory;
//! impl aerosol::Factory for StdoutLoggerFactory {
//! type Object = Arc<Logger>;
//! fn build(_: ()) -> Result<Arc<Logger>, anyhow::Error> {
//! Ok(Arc::new(StdoutLogger))
//! }
//! }
//!
//! // Part of our application does some work
//! aerosol::define_interface!(
//! WorkerInterface {
//! fn logger(&self) -> Arc<Logger>;
//! }
//! );
//!
//! fn do_work<I: WorkerInterface>(iface: I) {
//! iface.logger().log("Doing some work!");
//! }
//!
//! // Our application does multiple pieces of work
//! aerosol::define_interface!(
//! AppInterface: WorkerInterface + Clone {}
//! );
//!
//! fn run_app<I: AppInterface>(iface: I, num_work_items: usize) {
//! for _ in 0..num_work_items {
//! do_work(iface.clone());
//! }
//! }
//!
//! // At the very top level, we specify the implementations
//! // of our dependencies.
//! aerosol::define_context!(
//! AppContext {
//! logger: Arc<Logger> [StdoutLoggerFactory],
//! }
//! );
//!
//! let context = AppContext::new().unwrap();
//!
//! run_app(context, 4);
//! ```
//!
//! See the individual macro documentation for more details.
//! Provies integrations with the `axum` web framework. See the `axum` module
//! for more information.
#[doc(hidden)]
pub extern crate tt_call;
#[cfg(feature = "async")]
mod async_;
#[cfg(feature = "async")]
mod async_constructible;
#[cfg(feature = "axum")]
pub mod axum;
mod resource;
mod slot;
mod state;
mod sync;
mod sync_constructible;
mod context;
mod interface;
mod join;
mod parse;
pub use resource::Resource;
pub use state::Aerosol;
/// The building block for this crate. Automatically implemented
/// for contexts providing a dependency of type `T`.
///
/// Super-trait of all interfaces requiring a dependency of type
/// `T`.
pub trait Provide<T> {
fn provide(&self) -> T;
}
pub use sync_constructible::ConstructibleResource;
/// Implement this trait to provide a convenient syntax for
/// constructing implementations of dependencies.
pub trait Factory<Args = ()> {
type Object;
fn build(args: Args) -> Result<Self::Object, anyhow::Error>;
}
/// Allows cloning a context whilst replacing one dependency
/// with a different implementation. Must be explicitly listed
/// as a super-trait of an interface to use.
pub trait ProvideWith<T>: Provide<T> + Sized {
fn provide_with<E, F: FnOnce(T) -> Result<T, E>>(&self, f: F) -> Result<Self, E>;
}
#[cfg(feature = "async")]
pub use async_constructible::AsyncConstructibleResource;

View file

@ -1,73 +1 @@
#![recursion_limit = "512"]
#![allow(clippy::blacklisted_name)]
extern crate aerosol;
#[macro_use]
extern crate tt_call;
#[macro_export]
macro_rules! tt_debug2 {
{
$(
$output:ident = [{ $($tokens:tt)* }]
)*
} => {
$(
println!("{}",
concat!(
stringify!($output),
" = [{ ",
stringify!($($tokens)*),
" }]",
)
);
)*
}
}
aerosol::define_interface!(
TestInterface {
fn test_get(&self) -> Vec<u8>;
}
);
#[allow(dead_code)]
struct FooFactory;
#[derive(Clone, Debug)]
struct Foo;
#[derive(Clone, Debug)]
struct Bar;
impl aerosol::Factory<(Bar,)> for FooFactory {
type Object = Foo;
fn build(_: (Bar,)) -> Result<Foo, anyhow::Error> {
Ok(Foo)
}
}
aerosol::define_context!(
TestContext {
foo: Foo [(bar) FooFactory],
bar: Bar
}
);
fn main() {
//trace_macros!(true);
//aerosol::test_macro!();
tt_call! {
macro = [{ aerosol::private_define_interface }]
input = [{ TestInterface {
fn test_get(&self) -> Vec<u8>;
} }]
~~> tt_debug2
}
tt_call! {
macro = [{ aerosol::private_define_context }]
input = [{ TestContext {
db: MyDatabase [PostgresFactory<MyDatabase>],
pusher: PusherClient
} }]
~~> tt_debug2
}
}
fn main() {}

View file

@ -1,94 +0,0 @@
#[doc(hidden)]
#[macro_export]
macro_rules! parse_bound {
{
$caller:tt
input = [{ $($input:tt)* }]
} => {
$crate::parse_bound! {
$caller
rest = [{ $($input)* }]
}
};
{
$caller:tt
$(bound = [{ $($bound:tt)* }])*
rest = [{ $($rest:tt)* }]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::tt_call::parse_type }]
input = [{ $($rest)* }]
~~> $crate::parse_bound! {
$caller
$(bound = [{ $($bound)* }])*
}
}
};
{
$caller:tt
$(bound = [{ $($bound:tt)* }])*
type = [{ $($type:tt)* }]
rest = [{ + $($rest:tt)* }]
} => {
$crate::parse_bound! {
$caller
$(bound = [{ $($bound)* }])*
bound = [{ $($type)* }]
rest = [{ $($rest)* }]
}
};
{
$caller:tt
$(bound = [{ $($bound:tt)* }])*
type = [{ $($type:tt)* }]
rest = [{ $($rest:tt)* }]
} => {
$crate::tt_call::tt_return! {
$caller
$(bound = [{ $($bound)* }])*
bound = [{ $($type)* }]
rest = [{ $($rest)* }]
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! parse_trait_def {
{
$caller:tt
input = [{ $name:ident { $($body:tt)* } }]
} => {
$crate::tt_call::tt_return! {
$caller
name = [{ $name }]
body = [{ $($body)* }]
}
};
{
$caller:tt
input = [{ $name:ident: $($rest:tt)* }]
} => {
$crate::tt_call::tt_call! {
macro = [{ $crate::parse_bound }]
input = [{ $($rest)* }]
~~> $crate::parse_trait_def! {
$caller
name = [{ $name }]
}
}
};
{
$caller:tt
name = [{ $name:ident }]
$(bound = [{ $($bound:tt)* }])*
rest = [{ { $($body:tt)* } }]
} => {
$crate::tt_call::tt_return! {
$caller
name = [{ $name }]
body = [{ $($body)* }]
$(bound = [{ $($bound)* }])*
}
};
}

37
src/resource.rs Normal file
View file

@ -0,0 +1,37 @@
use std::{
any::{type_name, Any},
error::Error,
};
/// Bound on the types that can be used as an aerosol resource.
pub trait Resource: Any + Send + Sync + Clone {}
impl<T: Any + Send + Sync + Clone> Resource for T {}
pub(crate) fn unwrap_resource<T: Resource>(opt: Option<T>) -> T {
if let Some(value) = opt {
value
} else {
panic!("Resource `{}` does not exist", type_name::<T>())
}
}
pub(crate) fn unwrap_constructed<T: Resource, E: Error>(res: Result<T, E>) -> T {
match res {
Ok(x) => x,
Err(e) => panic!("Failed to construct `{}`: {}", type_name::<T>(), e),
}
}
pub(crate) fn duplicate_resource<T: Resource>() -> ! {
panic!(
"Duplicate resource: attempted to add a second `{}`",
type_name::<T>()
)
}
pub(crate) fn cyclic_resource<T: Resource>() -> ! {
panic!(
"Cycle detected when constructing resource `{}`",
type_name::<T>()
)
}

80
src/slot.rs Normal file
View file

@ -0,0 +1,80 @@
#[cfg(feature = "async")]
use std::task::Waker;
use std::thread::Thread;
use crate::resource::Resource;
#[derive(Debug, Clone)]
pub enum ThreadOrWaker {
Thread(Thread),
#[cfg(feature = "async")]
Waker(Waker),
}
impl PartialEq for ThreadOrWaker {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Thread(l0), Self::Thread(r0)) => l0.id() == r0.id(),
#[cfg(feature = "async")]
(Self::Waker(l0), Self::Waker(r0)) => l0.will_wake(r0),
#[cfg(feature = "async")]
_ => false,
}
}
}
impl From<Thread> for ThreadOrWaker {
fn from(value: Thread) -> Self {
Self::Thread(value)
}
}
#[cfg(feature = "async")]
impl From<&Waker> for ThreadOrWaker {
fn from(value: &Waker) -> Self {
Self::Waker(value.clone())
}
}
impl ThreadOrWaker {
pub fn unpark_or_wake(self) {
match self {
ThreadOrWaker::Thread(thread) => thread.unpark(),
#[cfg(feature = "async")]
ThreadOrWaker::Waker(waker) => waker.wake(),
}
}
}
pub enum Slot<T: Resource> {
Filled(T),
Placeholder {
owner: ThreadOrWaker,
waiting: Vec<ThreadOrWaker>,
},
}
impl<T: Resource> Slot<T> {
pub fn desc(&self) -> SlotDesc<T> {
if let Slot::Filled(x) = self {
SlotDesc::Filled(x.clone())
} else {
SlotDesc::Placeholder
}
}
}
impl<T: Resource> Drop for Slot<T> {
fn drop(&mut self) {
if let Self::Placeholder { waiting, .. } = self {
for item in waiting.drain(..) {
item.unpark_or_wake();
}
}
}
}
pub enum SlotDesc<T: Resource> {
Filled(T),
Placeholder,
}

108
src/state.rs Normal file
View file

@ -0,0 +1,108 @@
use std::{any::Any, sync::Arc, task::Poll};
use anymap::hashbrown::{Entry, Map};
use parking_lot::RwLock;
use crate::{
resource::{cyclic_resource, duplicate_resource, Resource},
slot::{Slot, SlotDesc, ThreadOrWaker},
};
#[derive(Debug, Default)]
struct InnerAerosol {
items: Map<dyn Any + Send + Sync>,
}
/// Stores a collection of resources keyed on resource type.
/// Provies methods for accessing this collection.
/// Can be cheaply cloned.
#[derive(Debug, Clone, Default)]
pub struct Aerosol {
inner: Arc<RwLock<InnerAerosol>>,
}
impl Aerosol {
/// Construct a new instance of the type with no initial resources.
pub fn new() -> Self {
Self::default()
}
/// Directly insert a resource into the collection. Panics if a resource of the
/// same type already exists.
pub fn insert<T: Resource>(&self, value: T) {
match self.inner.write().items.entry() {
Entry::Occupied(_) => duplicate_resource::<T>(),
Entry::Vacant(vac) => {
vac.insert(Slot::Filled(value));
}
}
}
/// Builder method equivalent to calling `insert()` but can be chained.
pub fn with<T: Resource>(self, value: T) -> Self {
self.insert(value);
self
}
pub(crate) fn try_get_slot<T: Resource>(&self) -> Option<SlotDesc<T>> {
self.inner.read().items.get().map(Slot::desc)
}
pub(crate) fn poll_for_slot<T: Resource, C: Into<ThreadOrWaker>>(
&self,
wait_index: &mut Option<usize>,
thread_or_waker_fn: impl Fn() -> C,
insert_placeholder: bool,
) -> Poll<Option<T>> {
let mut guard = self.inner.write();
match guard.items.entry::<Slot<T>>() {
Entry::Occupied(mut occ) => match occ.get_mut() {
Slot::Filled(x) => Poll::Ready(Some(x.clone())),
Slot::Placeholder { owner, waiting } => {
let current = thread_or_waker_fn().into();
if current == *owner {
cyclic_resource::<T>()
}
if let Some(idx) = *wait_index {
waiting[idx] = current;
} else {
*wait_index = Some(waiting.len());
waiting.push(current);
}
Poll::Pending
}
},
Entry::Vacant(vac) => {
if insert_placeholder {
vac.insert(Slot::Placeholder {
owner: thread_or_waker_fn().into(),
waiting: Vec::new(),
});
}
Poll::Ready(None)
}
}
}
pub(crate) fn fill_placeholder<T: Resource>(&self, value: T) {
self.inner.write().items.insert(Slot::Filled(value));
}
pub(crate) fn clear_placeholder<T: Resource>(&self) {
self.inner.write().items.remove::<Slot<T>>();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create() {
let state = Aerosol::new().with(42);
state.insert("Hello, world!");
}
#[test]
#[should_panic]
fn duplicate() {
let state = Aerosol::new().with(13);
state.insert(42);
}
}

75
src/sync.rs Normal file
View file

@ -0,0 +1,75 @@
use std::{task::Poll, thread};
use crate::{
resource::{unwrap_resource, Resource},
slot::SlotDesc,
state::Aerosol,
};
#[cfg(target_family = "wasm")]
pub fn safe_park() {
panic!("Cannot block on dependency construction on WASM")
}
#[cfg(not(target_family = "wasm"))]
pub fn safe_park() {
std::thread::park();
}
impl Aerosol {
/// Synchronously wait for the slot for `T` to not have a placeholder.
/// Returns immediately if there is no `T` present, or if `T`'s slot is filled.
pub(crate) fn wait_for_slot<T: Resource>(&self, insert_placeholder: bool) -> Option<T> {
let mut wait_index = None;
loop {
match self.poll_for_slot(&mut wait_index, thread::current, insert_placeholder) {
Poll::Pending => safe_park(),
Poll::Ready(x) => break x,
}
}
}
/// Tries to get an instance of `T` from the AppState. Returns `None` if there is no such instance.
/// This function does not attempt to construct `T` if it does not exist.
pub fn try_get<T: Resource>(&self) -> Option<T> {
match self.try_get_slot()? {
SlotDesc::Filled(x) => Some(x),
SlotDesc::Placeholder => self.wait_for_slot::<T>(false),
}
}
/// Get an instance of `T` from the AppState, and panic if not found.
/// This function does not attempt to construct `T` if it does not exist.
pub fn get<T: Resource>(&self) -> T {
unwrap_resource(self.try_get())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_with() {
let state = Aerosol::new().with(42);
assert_eq!(state.get::<i32>(), 42);
}
#[test]
fn get_inserted() {
let state = Aerosol::new();
state.insert(42);
assert_eq!(state.get::<i32>(), 42);
}
#[test]
fn try_get_some() {
let state = Aerosol::new().with(42);
assert_eq!(state.try_get::<i32>(), Some(42));
}
#[test]
fn try_get_none() {
let state = Aerosol::new().with("Hello");
assert_eq!(state.try_get::<i32>(), None);
}
}

123
src/sync_constructible.rs Normal file
View file

@ -0,0 +1,123 @@
use std::error::Error;
use crate::{
resource::{unwrap_constructed, Resource},
slot::SlotDesc,
state::Aerosol,
};
/// Implemented for resources which can be constructed from other resources.
pub trait ConstructibleResource: Resource {
/// Error type for when resource fails to be constructed.
type Error: Error + Send + Sync;
/// Construct the resource with the provided application state.
fn construct(aero: &Aerosol) -> Result<Self, Self::Error>;
}
impl Aerosol {
/// Try to get or construct an instance of `T`.
pub fn try_obtain<T: ConstructibleResource>(&self) -> Result<T, T::Error> {
match self.try_get_slot() {
Some(SlotDesc::Filled(x)) => Ok(x),
Some(SlotDesc::Placeholder) | None => match self.wait_for_slot::<T>(true) {
Some(x) => Ok(x),
None => match T::construct(self) {
Ok(x) => {
self.fill_placeholder::<T>(x.clone());
Ok(x)
}
Err(e) => {
self.clear_placeholder::<T>();
Err(e)
}
},
},
}
}
/// Get or construct an instance of `T`. Panics if unable.
pub fn obtain<T: ConstructibleResource>(&self) -> T {
unwrap_constructed(self.try_obtain::<T>())
}
}
#[cfg(test)]
mod tests {
use std::{convert::Infallible, thread::scope, time::Duration};
use super::*;
#[derive(Debug, Clone)]
struct Dummy;
impl ConstructibleResource for Dummy {
type Error = Infallible;
fn construct(_app_state: &Aerosol) -> Result<Self, Self::Error> {
std::thread::sleep(Duration::from_millis(100));
Ok(Self)
}
}
#[test]
fn obtain() {
let state = Aerosol::new();
state.obtain::<Dummy>();
}
#[test]
fn obtain_race() {
let state = Aerosol::new();
scope(|s| {
for _ in 0..100 {
s.spawn(|| state.obtain::<Dummy>());
}
});
}
#[derive(Debug, Clone)]
struct DummyRecursive;
impl ConstructibleResource for DummyRecursive {
type Error = Infallible;
fn construct(aero: &Aerosol) -> Result<Self, Self::Error> {
aero.obtain::<Dummy>();
Ok(Self)
}
}
#[test]
fn obtain_recursive() {
let state = Aerosol::new();
state.obtain::<DummyRecursive>();
}
#[test]
fn obtain_recursive_race() {
let state = Aerosol::new();
scope(|s| {
for _ in 0..100 {
s.spawn(|| state.obtain::<DummyRecursive>());
}
});
}
#[derive(Debug, Clone)]
struct DummyCyclic;
impl ConstructibleResource for DummyCyclic {
type Error = Infallible;
fn construct(aero: &Aerosol) -> Result<Self, Self::Error> {
aero.obtain::<DummyCyclic>();
Ok(Self)
}
}
#[test]
#[should_panic(expected = "Cycle detected")]
fn obtain_cyclic() {
let state = Aerosol::new();
state.obtain::<DummyCyclic>();
}
}