diff --git a/src/async_.rs b/src/async_.rs index 58911f7..393af03 100644 --- a/src/async_.rs +++ b/src/async_.rs @@ -5,18 +5,20 @@ use std::{ task::{Context, Poll}, }; -use frunk::prelude::HList; +use crate::{ + resource::{Resource, ResourceList}, + slot::SlotDesc, + state::Aero, +}; -use crate::{resource::Resource, slot::SlotDesc, state::Aerosol}; - -pub(crate) struct WaitForSlot { - state: Aerosol, +pub(crate) struct WaitForSlot { + state: Aero, wait_index: Option, insert_placeholder: bool, phantom: PhantomData T>, } -impl Future for WaitForSlot { +impl Future for WaitForSlot { type Output = Option; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -26,7 +28,7 @@ impl Future for WaitForSlot { } } -impl Aerosol { +impl Aero { pub(crate) fn wait_for_slot_async( &self, insert_placeholder: bool, @@ -54,13 +56,13 @@ mod tests { #[tokio::test] async fn try_get_some() { - let state = Aerosol::new().with(42); + let state = Aero::new().with(42); assert_eq!(state.try_get_async::().await, Some(42)); } #[tokio::test] async fn try_get_none() { - let state = Aerosol::new().with("Hello"); + let state = Aero::new().with("Hello"); assert_eq!(state.try_get_async::().await, None); } } diff --git a/src/async_constructible.rs b/src/async_constructible.rs index e9ed033..acdd132 100644 --- a/src/async_constructible.rs +++ b/src/async_constructible.rs @@ -1,12 +1,12 @@ -use std::{any::Any, sync::Arc}; +use std::{any::Any, marker::PhantomData, sync::Arc}; use async_trait::async_trait; -use frunk::prelude::HList; +use frunk::{hlist::Sculptor, HCons, HNil}; use crate::{ - resource::{unwrap_constructed, Resource}, + resource::{unwrap_constructed, unwrap_constructed_hlist, Resource, ResourceList}, slot::SlotDesc, - state::Aerosol, + state::Aero, sync_constructible::Constructible, }; @@ -17,13 +17,13 @@ pub trait AsyncConstructible: Sized + Any + Send + Sync { /// Error type for when resource fails to be constructed. type Error: Into + Send + Sync; /// Construct the resource with the provided application state. - async fn construct_async(aero: &Aerosol) -> Result; + async fn construct_async(aero: &Aero) -> Result; /// Called after construction with the concrete resource to allow the callee /// to provide additional resources. Can be used by eg. an `Arc` to also /// provide an implementation of `Arc`. async fn after_construction_async( _this: &(dyn Any + Send + Sync), - _aero: &Aerosol, + _aero: &Aero, ) -> Result<(), Self::Error> { Ok(()) } @@ -32,12 +32,12 @@ pub trait AsyncConstructible: Sized + Any + Send + Sync { #[async_trait] impl AsyncConstructible for T { type Error = ::Error; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { Self::construct(aero) } async fn after_construction_async( this: &(dyn Any + Send + Sync), - aero: &Aerosol, + aero: &Aero, ) -> Result<(), Self::Error> { Self::after_construction(this, aero) } @@ -50,13 +50,13 @@ pub trait IndirectlyAsyncConstructible: Sized + Any + Send + Sync { /// Error type for when resource fails to be constructed. type Error: Into + Send + Sync; /// Construct the resource with the provided application state. - async fn construct_async(aero: &Aerosol) -> Result; + async fn construct_async(aero: &Aero) -> Result; /// Called after construction with the concrete resource to allow the callee /// to provide additional resources. Can be used by eg. an `Arc` to also /// provide an implementation of `Arc`. async fn after_construction_async( _this: &(dyn Any + Send + Sync), - _aero: &Aerosol, + _aero: &Aero, ) -> Result<(), Self::Error> { Ok(()) } @@ -66,14 +66,14 @@ pub trait IndirectlyAsyncConstructible: Sized + Any + Send + Sync { impl IndirectlyAsyncConstructible for T { type Error = T::Error; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { let res = ::construct_async(aero).await?; ::after_construction_async(&res, aero).await?; Ok(res) } async fn after_construction_async( this: &(dyn Any + Send + Sync), - aero: &Aerosol, + aero: &Aero, ) -> Result<(), Self::Error> { ::after_construction_async(this, aero).await } @@ -86,13 +86,13 @@ macro_rules! impl_async_constructible { impl<$t: IndirectlyAsyncConstructible> IndirectlyAsyncConstructible for $x { type Error = $t::Error; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { let res = $y($t::construct_async(aero).await?); <$t as IndirectlyAsyncConstructible>::after_construction_async(&res, aero).await?; Ok(res) } - async fn after_construction_async(this: &(dyn Any + Send + Sync), aero: &Aerosol) -> Result<(), Self::Error> { + async fn after_construction_async(this: &(dyn Any + Send + Sync), aero: &Aero) -> Result<(), Self::Error> { <$t as IndirectlyAsyncConstructible>::after_construction_async(this, aero).await } } @@ -109,10 +109,36 @@ impl_async_constructible! { } /// Implemented for resources which can be asynchronously constructed from other resources. Requires feature `async`. +/// Do not implement this trait directly, instead implement `AsyncConstructible` and ensure +/// the remaining type bounds are met for the automatic implementation of `AsyncConstructibleResource`. pub trait AsyncConstructibleResource: Resource + IndirectlyAsyncConstructible {} impl AsyncConstructibleResource for T {} -impl Aerosol { +/// Automatically implemented for resource lists where every resource can be asynchronously constructed. +#[async_trait] +pub trait AsyncConstructibleResourceList: ResourceList { + /// Construct every resource in this list in the provided aerosol instance + async fn construct_async(aero: &Aero) -> anyhow::Result<()>; +} + +#[async_trait] +impl AsyncConstructibleResourceList for HNil { + async fn construct_async(_aero: &Aero) -> anyhow::Result<()> { + Ok(()) + } +} + +#[async_trait] +impl + AsyncConstructibleResourceList for HCons +{ + async fn construct_async(aero: &Aero) -> anyhow::Result<()> { + aero.try_init_async::().await.map_err(Into::into)?; + T::construct_async(aero).await + } +} + +impl Aero { /// Try to get or construct an instance of `T` asynchronously. Requires feature `async`. pub async fn try_obtain_async(&self) -> Result { match self.try_get_slot() { @@ -156,12 +182,58 @@ impl Aerosol { pub async fn init_async(&self) { unwrap_constructed::(self.try_init_async::().await) } + + /// Builder method equivalent to calling `try_init_async()` but can be chained. + pub async fn try_with_constructed_async( + self, + ) -> Result>, T::Error> { + self.try_init_async::().await?; + Ok(Aero { + inner: self.inner, + phantom: PhantomData, + }) + } + + /// Builder method equivalent to calling `try_init_async()` but can be chained. Panics if construction fails. + pub async fn with_constructed_async(self) -> Aero> { + unwrap_constructed::(self.try_with_constructed_async().await) + } + + /// Convert into a different variant of the Aero type. Any missing required resources + /// will be automatically asynchronously constructed. + pub async fn try_construct_remaining_async( + self, + ) -> anyhow::Result> + where + R2: Sculptor, + >::Remainder: AsyncConstructibleResourceList, + { + <>::Remainder>::construct_async(&self).await?; + Ok(Aero { + inner: self.inner, + phantom: PhantomData, + }) + } + + /// Convert into a different variant of the Aero type. Any missing required resources + /// will be automatically asynchronously constructed. Panics if construction of any missing resource fails. + pub async fn construct_remaining_async(self) -> Aero + where + R2: Sculptor, + >::Remainder: AsyncConstructibleResourceList, + { + unwrap_constructed_hlist::<>::Remainder, _>( + self.try_construct_remaining_async().await, + ) + } } #[cfg(test)] mod tests { use std::{convert::Infallible, time::Duration}; + use crate::Aero; + use super::*; #[derive(Debug, Clone)] @@ -171,7 +243,7 @@ mod tests { impl AsyncConstructible for Dummy { type Error = Infallible; - async fn construct_async(_app_state: &Aerosol) -> Result { + async fn construct_async(_app_state: &Aero) -> Result { tokio::time::sleep(Duration::from_millis(100)).await; Ok(Self) } @@ -179,13 +251,13 @@ mod tests { #[tokio::test] async fn obtain() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain_async::().await; } #[tokio::test] async fn obtain_race() { - let state = Aerosol::new(); + let state = Aero::new(); let mut handles = Vec::new(); for _ in 0..100 { let state = state.clone(); @@ -205,7 +277,7 @@ mod tests { impl AsyncConstructible for DummyRecursive { type Error = Infallible; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { aero.obtain_async::().await; Ok(Self) } @@ -213,13 +285,13 @@ mod tests { #[tokio::test] async fn obtain_recursive() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain_async::().await; } #[tokio::test] async fn obtain_recursive_race() { - let state = Aerosol::new(); + let state = Aero::new(); let mut handles = Vec::new(); for _ in 0..100 { let state = state.clone(); @@ -236,7 +308,7 @@ mod tests { impl AsyncConstructible for DummyCyclic { type Error = Infallible; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { aero.obtain_async::().await; Ok(Self) } @@ -245,7 +317,7 @@ mod tests { #[tokio::test] #[should_panic(expected = "Cycle detected")] async fn obtain_cyclic() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain_async::().await; } @@ -255,7 +327,7 @@ mod tests { impl Constructible for DummySync { type Error = Infallible; - fn construct(_app_state: &Aerosol) -> Result { + fn construct(_app_state: &Aero) -> Result { std::thread::sleep(Duration::from_millis(100)); Ok(Self) } @@ -268,7 +340,7 @@ mod tests { impl AsyncConstructible for DummySyncRecursive { type Error = Infallible; - async fn construct_async(aero: &Aerosol) -> Result { + async fn construct_async(aero: &Aero) -> Result { aero.obtain_async::().await; Ok(Self) } @@ -276,13 +348,13 @@ mod tests { #[tokio::test] async fn obtain_sync_recursive() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain_async::().await; } #[tokio::test] async fn obtain_sync_recursive_race() { - let state = Aerosol::new(); + let state = Aero::new(); let mut handles = Vec::new(); for _ in 0..100 { let state = state.clone(); @@ -299,7 +371,7 @@ mod tests { impl AsyncConstructible for DummyNonClone { type Error = Infallible; - async fn construct_async(_app_state: &Aerosol) -> Result { + async fn construct_async(_app_state: &Aero) -> Result { tokio::time::sleep(Duration::from_millis(100)).await; Ok(Self) } @@ -307,7 +379,7 @@ mod tests { #[tokio::test] async fn obtain_non_clone() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain_async::>().await; } @@ -322,13 +394,13 @@ mod tests { impl AsyncConstructible for DummyImpl { type Error = Infallible; - async fn construct_async(_app_state: &Aerosol) -> Result { + async fn construct_async(_app_state: &Aero) -> Result { Ok(Self) } async fn after_construction_async( this: &(dyn Any + Send + Sync), - aero: &Aerosol, + aero: &Aero, ) -> Result<(), Self::Error> { if let Some(arc) = this.downcast_ref::>() { aero.insert(arc.clone() as Arc) @@ -339,8 +411,29 @@ mod tests { #[tokio::test] async fn obtain_impl() { - let state = Aerosol::new(); + let state = Aero::new(); state.init_async::>().await; state.try_get_async::>().await.unwrap(); } + + #[tokio::test] + async fn with_constructed_async() { + let state = Aero::new() + .with(42) + .with_constructed_async::() + .await + .with("hi"); + state.get::(); + } + + #[tokio::test] + async fn construct_remaining_async() { + let state: Aero![i32, Dummy, DummyRecursive, &str] = Aero::new() + .with(42) + .with("hi") + .construct_remaining_async() + .await; + state.get::(); + state.get::(); + } } diff --git a/src/axum.rs b/src/axum.rs index b44aab0..b6f357b 100644 --- a/src/axum.rs +++ b/src/axum.rs @@ -4,7 +4,7 @@ //! resources from within route handlers. //! //! To make use of these extractors, your application state must either be -//! an `Aerosol`, or you must implement `FromRef` for `Aerosol`. +//! an `Aero`, or you must implement `FromRef` for `Aero`. use std::any::type_name; @@ -15,7 +15,7 @@ use axum::{ response::{IntoResponse, Response}, }; -use crate::{Aerosol, AsyncConstructibleResource, ConstructibleResource, Resource}; +use crate::{Aero, AsyncConstructibleResource, ConstructibleResource, Resource}; /// Type of axum Rejection returned when a resource cannot be acquired #[derive(Debug, thiserror::Error)] @@ -59,18 +59,18 @@ impl DependencyError { } } -/// Get an already-existing resource from the state. Equivalent to calling `Aerosol::try_get_async`. +/// Get an already-existing resource from the state. Equivalent to calling `Aero::try_get_async`. pub struct Dep(pub T); #[async_trait] impl FromRequestParts for Dep where - Aerosol: FromRef, + Aero: FromRef, { type Rejection = DependencyError; async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { - Aerosol::from_ref(state) + Aero::from_ref(state) .try_get_async() .await .map(Self) @@ -78,18 +78,18 @@ where } } -/// Get a resource from the state, or construct it if it doesn't exist. Equivalent to calling `Aerosol::try_obtain_async`. +/// Get a resource from the state, or construct it if it doesn't exist. Equivalent to calling `Aero::try_obtain_async`. pub struct Obtain(pub T); #[async_trait] impl FromRequestParts for Obtain where - Aerosol: FromRef, + Aero: FromRef, { type Rejection = DependencyError; async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { - Aerosol::from_ref(state) + Aero::from_ref(state) .try_obtain_async() .await .map(Self) diff --git a/src/lib.rs b/src/lib.rs index 9214ccf..ff86ad3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,127 @@ #![deny(missing_docs)] //! # aerosol -//! Simple dependency injection for Rust +//! Simple but powerful dependency injection for Rust. //! -//! Optional features: +//! This crate provides the `Aero` type, which stores dependencies (called resources) keyed by their +//! type. Resources can be constructed eagerly at application startup, or on-demand when they are +//! first needed. Resources can access and/or initialize other resources on creation. //! -//! ## `async` +//! The crate will detect dependency cycles (if constructing resource A requires resource B which +//! itself requires resource A) and will panic rather than stack overflow in that case. +//! +//! The `Aero` type has an optional type parameter to make certain resources *required*. When +//! a resource is required it can be accessed infallibly. The `Aero![...]` macro exists to +//! easily name an `Aero` with a specific set of required resources. +//! +//! ## Optional features +//! +//! ### `async` //! //! Allows resources to be constructed asynchrously, and provides a corresponding //! `AsyncConstructibleResource` trait. //! -//! ## `axum` +//! ### `axum` //! //! Provides integrations with the `axum` web framework. See the `axum` module //! for more information. +//! +//! ## Example usage +//! +//! ```rust +//! use std::{sync::Arc, any::Any}; +//! +//! # struct PostmarkClient; +//! # #[derive(Clone)] +//! # struct ConnectionPool; +//! # #[derive(Clone)] +//! # struct MessageQueue; +//! # #[derive(Clone)] +//! # struct MagicNumber(i32); +//! # trait EmailSender: Send + Sync { fn send(&self) {} } +//! # impl EmailSender for PostmarkClient {} +//! # impl PostmarkClient { fn new() -> anyhow::Result { Ok(Self) }} +//! use aerosol::{Aero, Constructible}; +//! +//! // Here, we can list all the things we want to guarantee are in +//! // our app state. This is entirely optional, we could also just +//! // use the `Aero` type with default arguments and check that +//! // resources are present at runtime. +//! type AppState = Aero![ +//! Arc, +//! Arc, +//! ConnectionPool, +//! MessageQueue, +//! MagicNumber, +//! ]; +//! +//! fn main() { +//! let app_state: AppState = Aero::new() +//! // Directly add a resource which doesn't implement `Constructible`. +//! .with(MagicNumber(42)) +//! // Construct an `Arc` resource in the AppState +//! .with_constructed::>() +//! // Check that an implementation of `EmailSender` was added as a result +//! .assert::>() +//! // Automatically construct anything else necessary for our AppState +//! // (in this case, `ConnectionPool` and `MessageQueue`) +//! .construct_remaining(); +//! +//! // Add an extra resource +//! app_state.insert("Hello, world"); +//! +//! run(app_state); +//! } +//! +//! fn run(app_state: AppState) { +//! // The `get()` method is infallible because the `Arc` was +//! // explicitly listed when defining our `AppState`. +//! let email_sender: Arc = app_state.get(); +//! email_sender.send(/* email */); +//! +//! // We have to use `try_get()` here because a `&str` is not guaranteed to +//! // exist on our `AppState`. +//! let hello_message: &str = app_state.try_get().unwrap(); +//! println!("{hello_message}"); +//! +//! // ... more application logic +//! } +//! +//! // The `Constructible` trait can be implemented to allow resources to be automatically +//! // constructed. +//! impl Constructible for PostmarkClient { +//! type Error = anyhow::Error; +//! +//! fn construct(aero: &Aero) -> Result { +//! PostmarkClient::new(/* initialize using environment variables */) +//! } +//! +//! fn after_construction(this: &(dyn Any + Send + Sync), aero: &Aero) -> Result<(), Self::Error> { +//! // We can use this to automatically populate extra resources on the context. +//! // For example, in this case we can make it so that if an `Arc` gets +//! // constructed, we also provide `Arc`. +//! if let Some(arc) = this.downcast_ref::>() { +//! aero.insert(arc.clone() as Arc) +//! } +//! Ok(()) +//! } +//! } +//! +//! impl Constructible for ConnectionPool { +//! type Error = anyhow::Error; +//! fn construct(aero: &Aero) -> Result { +//! // ... +//! # Ok(ConnectionPool) +//! } +//! } +//! +//! impl Constructible for MessageQueue { +//! type Error = anyhow::Error; +//! fn construct(aero: &Aero) -> Result { +//! // ... +//! # Ok(MessageQueue) +//! } +//! } +//! ``` pub use frunk; #[cfg(feature = "async")] @@ -28,12 +137,15 @@ mod state; mod sync; mod sync_constructible; -pub use resource::Resource; -pub use state::Aerosol; +pub use resource::{Resource, ResourceList}; +pub use state::Aero; -pub use sync_constructible::{Constructible, ConstructibleResource, IndirectlyConstructible}; +pub use sync_constructible::{ + Constructible, ConstructibleResource, ConstructibleResourceList, IndirectlyConstructible, +}; #[cfg(feature = "async")] pub use async_constructible::{ - AsyncConstructible, AsyncConstructibleResource, IndirectlyAsyncConstructible, + AsyncConstructible, AsyncConstructibleResource, AsyncConstructibleResourceList, + IndirectlyAsyncConstructible, }; diff --git a/src/macros.rs b/src/macros.rs index 590265b..8601803 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,7 +1,14 @@ -/// Define a custom `Aerosol` alias with a specific set of required types +/// Define a custom `Aero` alias with a specific set of required types +/// +/// Example usage: +/// ```rust +/// use aerosol::Aero; +/// +/// type AppState = Aero![&'static str, i32, bool]; +/// ``` #[macro_export] macro_rules! Aero { ($($tok:tt)*) => { - $crate::Aerosol<$crate::frunk::HList![$($tok)*]> + $crate::Aero<$crate::frunk::HList![$($tok)*]> }; } diff --git a/src/resource.rs b/src/resource.rs index 9c07a97..63cde6f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,14 +1,38 @@ use std::any::{type_name, Any}; +use frunk::{prelude::HList, HCons, HNil}; + +use crate::Aero; + /// Bound on the types that can be used as an aerosol resource. pub trait Resource: Any + Send + Sync + Clone {} impl Resource for T {} +/// A compile-time list of resource types which are statically guaranteed to be present. +pub trait ResourceList: HList + Any + Send + Sync + Clone { + /// Test at runtmie whether every resource in this list is present in the given Aero instance. + fn test(aero: &Aero) -> bool; +} +impl ResourceList for HNil { + fn test(_aero: &Aero) -> bool { + true + } +} +impl ResourceList for HCons { + fn test(aero: &Aero) -> bool { + aero.has::() && T::test(aero) + } +} + +pub(crate) fn missing_resource() -> ! { + panic!("Resource `{}` does not exist", type_name::()) +} + pub(crate) fn unwrap_resource(opt: Option) -> T { if let Some(value) = opt { value } else { - panic!("Resource `{}` does not exist", type_name::()) + missing_resource::() } } @@ -19,6 +43,17 @@ pub(crate) fn unwrap_constructed(res: Result(res: Result>) -> U { + match res { + Ok(x) => x, + Err(e) => panic!( + "Failed to construct one of `{}`: {}", + type_name::(), + e.into() + ), + } +} + pub(crate) fn duplicate_resource() -> ! { panic!( "Duplicate resource: attempted to add a second `{}`", diff --git a/src/state.rs b/src/state.rs index 256396c..9b6329e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,18 +3,17 @@ use std::{any::Any, marker::PhantomData, sync::Arc, task::Poll}; use anymap::hashbrown::{Entry, Map}; use frunk::{ hlist::{HFoldRightable, Sculptor}, - prelude::HList, HCons, HNil, Poly, }; use parking_lot::RwLock; use crate::{ - resource::{cyclic_resource, duplicate_resource, Resource}, + resource::{cyclic_resource, duplicate_resource, missing_resource, Resource, ResourceList}, slot::{Slot, SlotDesc, ThreadOrWaker}, }; #[derive(Debug, Default)] -struct InnerAerosol { +pub(crate) struct InnerAero { items: Map, } @@ -23,12 +22,12 @@ struct InnerAerosol { /// Can be cheaply cloned. #[derive(Debug)] #[repr(transparent)] -pub struct Aerosol { - inner: Arc>, - phantom: PhantomData>, +pub struct Aero { + pub(crate) inner: Arc>, + pub(crate) phantom: PhantomData>, } -impl Aerosol { +impl Aero { /// Construct a new instance of the type with no initial resources. pub fn new() -> Self { Self { @@ -38,7 +37,7 @@ impl Aerosol { } } -impl Clone for Aerosol { +impl Clone for Aero { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -47,41 +46,24 @@ impl Clone for Aerosol { } } -struct AerosolBuilderFolder; -impl frunk::Func<(Aerosol, T)> for AerosolBuilderFolder { - type Output = Aerosol>; - fn call((aero, value): (Aerosol, T)) -> Self::Output { +struct AerosolDefaultFolder; +impl frunk::Func<(Aero, T)> for AerosolDefaultFolder { + type Output = Aero>; + fn call((aero, value): (Aero, T)) -> Self::Output { aero.with(value) } } -#[doc(hidden)] -pub trait HTestable { - fn test(aero: &Aerosol) -> bool; -} - -impl HTestable for HNil { - fn test(_aero: &Aerosol) -> bool { - true - } -} - -impl HTestable for HCons { - fn test(aero: &Aerosol) -> bool { - aero.has::() && T::test(aero) - } -} - impl< - R: Default + HFoldRightable, Aerosol, Output = Aerosol> + HList, - > Default for Aerosol + R: Default + HFoldRightable, Aero, Output = Aero> + ResourceList, + > Default for Aero { fn default() -> Self { - R::default().foldr(Poly(AerosolBuilderFolder), Aerosol::new()) + R::default().foldr(Poly(AerosolDefaultFolder), Aero::new()) } } -impl Aerosol { +impl Aero { /// Directly insert a resource into the collection. Panics if a resource of the /// same type already exists. pub fn insert(&self, value: T) { @@ -94,44 +76,44 @@ impl Aerosol { } /// Builder method equivalent to calling `insert()` but can be chained. - pub fn with(self, value: T) -> Aerosol> { + pub fn with(self, value: T) -> Aero> { self.insert(value); - Aerosol { + Aero { inner: self.inner, phantom: PhantomData, } } - /// Convert into a different variant of the Aerosol type. The new variant must + /// Convert into a different variant of the Aero type. The new variant must /// not require any resources which are not required as part of this type. - pub fn into(self) -> Aerosol + pub fn into(self) -> Aero where R: Sculptor, { - Aerosol { + Aero { inner: self.inner, phantom: PhantomData, } } - /// Reborrow as a different variant of the Aerosol type. The new variant must + /// Reborrow as a different variant of the Aero type. The new variant must /// not require any resources which are not required as part of this type. #[allow(clippy::should_implement_trait)] - pub fn as_ref(&self) -> &Aerosol + pub fn as_ref(&self) -> &Aero where R: Sculptor, { - // Safety: all Aerosol variants are `#[repr(transparent)]` wrappers around + // Safety: all Aero variants are `#[repr(transparent)]` wrappers around // the same concrete type. unsafe { std::mem::transmute(self) } } - /// Try to convert into a different variant of the Aerosol type. Returns the + /// Try to convert into a different variant of the Aero type. Returns the /// original type if one or more of the required resources are not fully /// constructed. - pub fn try_into(self) -> Result, Self> { + pub fn try_into(self) -> Result, Self> { if R2::test(&self) { - Ok(Aerosol { + Ok(Aero { inner: self.inner, phantom: PhantomData, }) @@ -140,13 +122,13 @@ impl Aerosol { } } - /// Try to convert into a different variant of the Aerosol type. Returns + /// Try to convert into a different variant of the Aero type. Returns /// `None` if one or more of the required resources are not fully /// constructed. - pub fn try_as_ref(&self) -> Option<&Aerosol> { + pub fn try_as_ref(&self) -> Option<&Aero> { if R2::test(self) { Some( - // Safety: all Aerosol variants are `#[repr(transparent)]` wrappers around + // Safety: all Aero variants are `#[repr(transparent)]` wrappers around // the same concrete type. unsafe { std::mem::transmute(self) }, ) @@ -164,6 +146,24 @@ impl Aerosol { ) } + /// Assert that a resource exists, returns `self` unchanged if not + pub fn try_assert(self) -> Result>, Self> { + if self.has::() { + Ok(Aero { + inner: self.inner, + phantom: PhantomData, + }) + } else { + Err(self) + } + } + + /// Assert that a resource exists, panic if not + pub fn assert(self) -> Aero> { + self.try_assert() + .unwrap_or_else(|_| missing_resource::()) + } + pub(crate) fn try_get_slot(&self) -> Option> { self.inner.read().items.get().map(Slot::desc) } @@ -211,14 +211,14 @@ impl Aerosol { } } -impl AsRef for Aerosol { - fn as_ref(&self) -> &Aerosol { - Aerosol::as_ref(self) +impl AsRef for Aero { + fn as_ref(&self) -> &Aero { + Aero::as_ref(self) } } -impl From>> for Aerosol { - fn from(value: Aerosol>) -> Self { +impl From>> for Aero { + fn from(value: Aero>) -> Self { value.into() } } @@ -227,32 +227,37 @@ impl From>> for Aerosol { mod tests { use crate::Aero; - use super::*; - #[test] fn create() { - let state = Aerosol::new().with(42); + let state = Aero::new().with(42); state.insert("Hello, world!"); } #[test] #[should_panic] fn duplicate() { - let state = Aerosol::new().with(13); + let state = Aero::new().with(13); state.insert(42); } #[test] fn default() { - let state: Aero![i32] = Aerosol::default(); + let state: Aero![i32] = Aero::default(); state.insert("Hello, world!"); } #[test] fn convert() { - let state: Aero![i32, String, f32] = Aerosol::default(); + let state: Aero![i32, String, f32] = Aero::default(); state.insert("Hello, world!"); let state2: Aero![f32, String] = state.into(); let _state3: Aero![i32, String, f32] = state2.try_into().unwrap(); } + + #[test] + fn assert() { + let state: Aero![i32, String, f32] = Aero::default(); + state.insert("Hello, world!"); + let _state2: Aero![&str, f32] = state.assert::<&str>().into(); + } } diff --git a/src/sync.rs b/src/sync.rs index 4ed4b86..afcf8c8 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,11 +1,11 @@ use std::{task::Poll, thread}; -use frunk::{hlist::Plucker, prelude::HList}; +use frunk::hlist::Plucker; use crate::{ - resource::{unwrap_resource, Resource}, + resource::{unwrap_resource, Resource, ResourceList}, slot::SlotDesc, - state::Aerosol, + state::Aero, }; #[cfg(target_family = "wasm")] @@ -18,7 +18,7 @@ pub fn safe_park() { std::thread::park(); } -impl Aerosol { +impl Aero { /// 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(&self, insert_placeholder: bool) -> Option { @@ -54,19 +54,19 @@ mod tests { #[test] fn get_with() { - let state = Aerosol::new().with(42); + let state = Aero::new().with(42); assert_eq!(state.get::(), 42); } #[test] fn try_get_some() { - let state = Aerosol::new().with(42); + let state = Aero::new().with(42); assert_eq!(state.try_get::(), Some(42)); } #[test] fn try_get_none() { - let state = Aerosol::new().with("Hello"); + let state = Aero::new().with("Hello"); assert_eq!(state.try_get::(), None); } } diff --git a/src/sync_constructible.rs b/src/sync_constructible.rs index 8572278..aa81728 100644 --- a/src/sync_constructible.rs +++ b/src/sync_constructible.rs @@ -1,11 +1,11 @@ -use std::{any::Any, sync::Arc}; +use std::{any::Any, marker::PhantomData, sync::Arc}; -use frunk::prelude::HList; +use frunk::{hlist::Sculptor, HCons, HNil}; use crate::{ - resource::{unwrap_constructed, Resource}, + resource::{unwrap_constructed, unwrap_constructed_hlist, Resource, ResourceList}, slot::SlotDesc, - state::Aerosol, + state::Aero, }; /// Implemented for values which can be constructed from other resources. @@ -13,14 +13,14 @@ pub trait Constructible: Sized + Any + Send + Sync { /// Error type for when resource fails to be constructed. type Error: Into + Send + Sync; /// Construct the resource with the provided application state. - fn construct(aero: &Aerosol) -> Result; + fn construct(aero: &Aero) -> Result; /// Called after construction with the concrete resource to allow the callee /// to provide additional resources. Can be used by eg. an `Arc` to also /// provide an implementation of `Arc`. fn after_construction( _this: &(dyn Any + Send + Sync), - _aero: &Aerosol, + _aero: &Aero, ) -> Result<(), Self::Error> { Ok(()) } @@ -31,13 +31,13 @@ pub trait IndirectlyConstructible: Sized + Any + Send + Sync { /// Error type for when resource fails to be constructed. type Error: Into + Send + Sync; /// Construct the resource with the provided application state. - fn construct(aero: &Aerosol) -> Result; + fn construct(aero: &Aero) -> Result; /// Called after construction with the concrete resource to allow the callee /// to provide additional resources. Can be used by eg. an `Arc` to also /// provide an implementation of `Arc`. fn after_construction( _this: &(dyn Any + Send + Sync), - _aero: &Aerosol, + _aero: &Aero, ) -> Result<(), Self::Error> { Ok(()) } @@ -46,16 +46,13 @@ pub trait IndirectlyConstructible: Sized + Any + Send + Sync { impl IndirectlyConstructible for T { type Error = T::Error; - fn construct(aero: &Aerosol) -> Result { + fn construct(aero: &Aero) -> Result { let res = ::construct(aero)?; ::after_construction(&res, aero)?; Ok(res) } - fn after_construction( - this: &(dyn Any + Send + Sync), - aero: &Aerosol, - ) -> Result<(), Self::Error> { + fn after_construction(this: &(dyn Any + Send + Sync), aero: &Aero) -> Result<(), Self::Error> { ::after_construction(this, aero) } } @@ -66,13 +63,13 @@ macro_rules! impl_constructible { impl<$t: IndirectlyConstructible> IndirectlyConstructible for $x { type Error = $t::Error; - fn construct(aero: &Aerosol) -> Result { + fn construct(aero: &Aero) -> Result { let res = $y($t::construct(aero)?); <$t as IndirectlyConstructible>::after_construction(&res, aero)?; Ok(res) } - fn after_construction(this: &(dyn Any + Send + Sync), aero: &Aerosol) -> Result<(), Self::Error> { + fn after_construction(this: &(dyn Any + Send + Sync), aero: &Aero) -> Result<(), Self::Error> { <$t as IndirectlyConstructible>::after_construction(this, aero) } } @@ -89,10 +86,33 @@ impl_constructible! { } /// Implemented for resources which can be constructed from other resources. +/// Do not implement this trait directly, instead implement `Constructible` and ensure +/// the remaining type bounds are met for the automatic implementation of `ConstructibleResource`. pub trait ConstructibleResource: Resource + IndirectlyConstructible {} impl ConstructibleResource for T {} -impl Aerosol { +/// Automatically implemented for resource lists where every resource can be constructed. +pub trait ConstructibleResourceList: ResourceList { + /// Construct every resource in this list in the provided aerosol instance + fn construct(aero: &Aero) -> anyhow::Result<()>; +} + +impl ConstructibleResourceList for HNil { + fn construct(_aero: &Aero) -> anyhow::Result<()> { + Ok(()) + } +} + +impl ConstructibleResourceList + for HCons +{ + fn construct(aero: &Aero) -> anyhow::Result<()> { + aero.try_init::().map_err(Into::into)?; + T::construct(aero) + } +} + +impl Aero { /// Try to get or construct an instance of `T`. pub fn try_obtain(&self) -> Result { match self.try_get_slot() { @@ -136,12 +156,56 @@ impl Aerosol { pub fn init(&self) { unwrap_constructed::(self.try_init::()) } + + /// Builder method equivalent to calling `try_init()` but can be chained. + pub fn try_with_constructed( + self, + ) -> Result>, T::Error> { + self.try_init::()?; + Ok(Aero { + inner: self.inner, + phantom: PhantomData, + }) + } + + /// Builder method equivalent to calling `try_init()` but can be chained. Panics if construction fails. + pub fn with_constructed(self) -> Aero> { + unwrap_constructed::(self.try_with_constructed()) + } + + /// Convert into a different variant of the Aero type. Any missing required resources + /// will be automatically constructed. + pub fn try_construct_remaining(self) -> anyhow::Result> + where + R2: Sculptor, + >::Remainder: ConstructibleResourceList, + { + <>::Remainder>::construct(&self)?; + Ok(Aero { + inner: self.inner, + phantom: PhantomData, + }) + } + + /// Convert into a different variant of the Aero type. Any missing required resources + /// will be automatically constructed. Panics if construction of any missing resource fails. + pub fn construct_remaining(self) -> Aero + where + R2: Sculptor, + >::Remainder: ConstructibleResourceList, + { + unwrap_constructed_hlist::<>::Remainder, _>( + self.try_construct_remaining(), + ) + } } #[cfg(test)] mod tests { use std::{convert::Infallible, thread::scope, time::Duration}; + use crate::Aero; + use super::*; #[derive(Debug, Clone)] @@ -150,7 +214,7 @@ mod tests { impl Constructible for Dummy { type Error = Infallible; - fn construct(_app_state: &Aerosol) -> Result { + fn construct(_app_state: &Aero) -> Result { std::thread::sleep(Duration::from_millis(100)); Ok(Self) } @@ -158,13 +222,13 @@ mod tests { #[test] fn obtain() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain::(); } #[test] fn obtain_race() { - let state = Aerosol::new(); + let state = Aero::new(); scope(|s| { for _ in 0..100 { s.spawn(|| state.obtain::()); @@ -178,7 +242,7 @@ mod tests { impl Constructible for DummyRecursive { type Error = Infallible; - fn construct(aero: &Aerosol) -> Result { + fn construct(aero: &Aero) -> Result { aero.obtain::(); Ok(Self) } @@ -186,13 +250,13 @@ mod tests { #[test] fn obtain_recursive() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain::(); } #[test] fn obtain_recursive_race() { - let state = Aerosol::new(); + let state = Aero::new(); scope(|s| { for _ in 0..100 { s.spawn(|| state.obtain::()); @@ -206,7 +270,7 @@ mod tests { impl Constructible for DummyCyclic { type Error = Infallible; - fn construct(aero: &Aerosol) -> Result { + fn construct(aero: &Aero) -> Result { aero.obtain::(); Ok(Self) } @@ -215,7 +279,7 @@ mod tests { #[test] #[should_panic(expected = "Cycle detected")] fn obtain_cyclic() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain::(); } @@ -225,7 +289,7 @@ mod tests { impl Constructible for DummyNonClone { type Error = Infallible; - fn construct(_app_state: &Aerosol) -> Result { + fn construct(_app_state: &Aero) -> Result { std::thread::sleep(Duration::from_millis(100)); Ok(Self) } @@ -233,7 +297,7 @@ mod tests { #[test] fn obtain_non_clone() { - let state = Aerosol::new(); + let state = Aero::new(); state.obtain::>(); } @@ -247,13 +311,13 @@ mod tests { impl Constructible for DummyImpl { type Error = Infallible; - fn construct(_app_state: &Aerosol) -> Result { + fn construct(_app_state: &Aero) -> Result { Ok(Self) } fn after_construction( this: &(dyn Any + Send + Sync), - aero: &Aerosol, + aero: &Aero, ) -> Result<(), Self::Error> { if let Some(arc) = this.downcast_ref::>() { aero.insert(arc.clone() as Arc) @@ -264,8 +328,22 @@ mod tests { #[test] fn obtain_impl() { - let state = Aerosol::new(); + let state = Aero::new(); state.init::>(); state.try_get::>().unwrap(); } + + #[test] + fn with_constructed() { + let state = Aero::new().with(42).with_constructed::().with("hi"); + state.get::(); + } + + #[test] + fn construct_remaining() { + let state: Aero![i32, Dummy, DummyRecursive, &str] = + Aero::new().with(42).with("hi").construct_remaining(); + state.get::(); + state.get::(); + } }