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

View file

@ -66,14 +66,14 @@ jobs:
args: --all-features
test_minimum:
name: Test (1.35.0)
name: Test (1.70.0)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.35.0
toolchain: 1.70.0
override: true
- uses: actions-rs/cargo@v1
with:

View file

@ -1,12 +1,25 @@
[package]
name = "aerosol"
version = "0.3.0"
version = "1.0.0-alpha.1"
authors = ["Diggory Blake <diggsey@googlemail.com>"]
edition = "2018"
description = "Dependency injection with compile-time guarantees"
description = "Simple dependency injection for Rust"
repository = "https://github.com/Diggsey/aerosol"
license = "MIT OR Apache-2.0"
[features]
default = []
async = ["async-trait"]
axum = ["dep:axum", "async", "tracing", "thiserror", "anyhow"]
[dependencies]
tt-call = "1.0"
anyhow = "1"
parking_lot = "0.12.1"
anymap = { version = "1.0.0-beta.2", features = ["hashbrown"] }
async-trait = { version = "0.1", optional = true }
axum = { version = "0.6", optional = true }
tracing = { version = "0.1", optional = true }
thiserror = { version = "1.0", optional = true }
anyhow = { version = "1.0", optional = true }
[dev-dependencies]
tokio = { version = "1.0", features = ["macros"] }

View file

@ -3,13 +3,3 @@
Simple dependency injection for Rust
[Documentation](https://docs.rs/aerosol/)
The two main exports of this crate are the `define_context!` and `define_interface!` macros.
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.
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.
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.
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.

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>();
}
}