mirror of
https://github.com/TECHNOFAB11/aerosol.git
synced 2026-02-02 09:25:11 +01:00
New approach to dependency injection for 1.x
This commit is contained in:
parent
e94a833eaf
commit
f8ebe14790
17 changed files with 840 additions and 822 deletions
86
src/async_.rs
Normal file
86
src/async_.rs
Normal 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
189
src/async_constructible.rs
Normal 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
98
src/axum.rs
Normal 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>)
|
||||
}
|
||||
}
|
||||
277
src/context.rs
277
src/context.rs
|
|
@ -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)* }]
|
||||
}
|
||||
);
|
||||
}
|
||||
192
src/interface.rs
192
src/interface.rs
|
|
@ -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)* }]
|
||||
}
|
||||
);
|
||||
}
|
||||
56
src/join.rs
56
src/join.rs
|
|
@ -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 ");
|
||||
}
|
||||
138
src/lib.rs
138
src/lib.rs
|
|
@ -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;
|
||||
|
|
|
|||
74
src/main.rs
74
src/main.rs
|
|
@ -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() {}
|
||||
|
|
|
|||
94
src/parse.rs
94
src/parse.rs
|
|
@ -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
37
src/resource.rs
Normal 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
80
src/slot.rs
Normal 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
108
src/state.rs
Normal 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
75
src/sync.rs
Normal 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
123
src/sync_constructible.rs
Normal 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>();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue