Implement statically required resources

This commit is contained in:
Diggory Blake 2023-08-03 19:56:48 +01:00
parent 3d7c0bed49
commit a5395a5d33
No known key found for this signature in database
GPG key ID: E6BDFA83146ABD40
9 changed files with 195 additions and 56 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "aerosol" name = "aerosol"
version = "1.0.0-alpha.4" version = "1.0.0-alpha.5"
authors = ["Diggory Blake <diggsey@googlemail.com>"] authors = ["Diggory Blake <diggsey@googlemail.com>"]
edition = "2018" edition = "2018"
description = "Simple dependency injection for Rust" description = "Simple dependency injection for Rust"
@ -23,6 +23,7 @@ axum = { version = "0.6", optional = true }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
thiserror = { version = "1.0", optional = true } thiserror = { version = "1.0", optional = true }
anyhow = { version = "1.0" } anyhow = { version = "1.0" }
frunk = "0.4.2"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.0", features = ["macros"] } tokio = { version = "1.0", features = ["macros"] }

View file

@ -5,20 +5,18 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use crate::{ use frunk::prelude::HList;
resource::{unwrap_resource, Resource},
slot::SlotDesc,
state::Aerosol,
};
pub(crate) struct WaitForSlot<T: Resource> { use crate::{resource::Resource, slot::SlotDesc, state::Aerosol};
state: Aerosol,
pub(crate) struct WaitForSlot<R: HList, T: Resource> {
state: Aerosol<R>,
wait_index: Option<usize>, wait_index: Option<usize>,
insert_placeholder: bool, insert_placeholder: bool,
phantom: PhantomData<fn() -> T>, phantom: PhantomData<fn() -> T>,
} }
impl<T: Resource> Future for WaitForSlot<T> { impl<R: HList, T: Resource> Future for WaitForSlot<R, T> {
type Output = Option<T>; type Output = Option<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
@ -28,11 +26,11 @@ impl<T: Resource> Future for WaitForSlot<T> {
} }
} }
impl Aerosol { impl<R: HList> Aerosol<R> {
pub(crate) fn wait_for_slot_async<T: Resource>( pub(crate) fn wait_for_slot_async<T: Resource>(
&self, &self,
insert_placeholder: bool, insert_placeholder: bool,
) -> WaitForSlot<T> { ) -> WaitForSlot<R, T> {
WaitForSlot { WaitForSlot {
state: self.clone(), state: self.clone(),
wait_index: None, wait_index: None,
@ -48,30 +46,12 @@ impl Aerosol {
SlotDesc::Placeholder => self.wait_for_slot_async::<T>(false).await, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[tokio::test]
async fn try_get_some() { async fn try_get_some() {
let state = Aerosol::new().with(42); let state = Aerosol::new().with(42);

View file

@ -1,6 +1,7 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use frunk::prelude::HList;
use crate::{ use crate::{
resource::{unwrap_constructed, Resource}, resource::{unwrap_constructed, Resource},
@ -111,14 +112,14 @@ impl_async_constructible! {
pub trait AsyncConstructibleResource: Resource + IndirectlyAsyncConstructible {} pub trait AsyncConstructibleResource: Resource + IndirectlyAsyncConstructible {}
impl<T: Resource + IndirectlyAsyncConstructible> AsyncConstructibleResource for T {} impl<T: Resource + IndirectlyAsyncConstructible> AsyncConstructibleResource for T {}
impl Aerosol { impl<R: HList> Aerosol<R> {
/// Try to get or construct an instance of `T` asynchronously. Requires feature `async`. /// 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> { pub async fn try_obtain_async<T: AsyncConstructibleResource>(&self) -> Result<T, T::Error> {
match self.try_get_slot() { match self.try_get_slot() {
Some(SlotDesc::Filled(x)) => Ok(x), Some(SlotDesc::Filled(x)) => Ok(x),
Some(SlotDesc::Placeholder) | None => match self.wait_for_slot_async::<T>(true).await { Some(SlotDesc::Placeholder) | None => match self.wait_for_slot_async::<T>(true).await {
Some(x) => Ok(x), Some(x) => Ok(x),
None => match T::construct_async(self).await { None => match T::construct_async(self.as_ref()).await {
Ok(x) => { Ok(x) => {
self.fill_placeholder::<T>(x.clone()); self.fill_placeholder::<T>(x.clone());
Ok(x) Ok(x)
@ -139,7 +140,7 @@ impl Aerosol {
pub async fn try_init_async<T: AsyncConstructibleResource>(&self) -> Result<(), T::Error> { pub async fn try_init_async<T: AsyncConstructibleResource>(&self) -> Result<(), T::Error> {
match self.wait_for_slot_async::<T>(true).await { match self.wait_for_slot_async::<T>(true).await {
Some(_) => Ok(()), Some(_) => Ok(()),
None => match T::construct_async(self).await { None => match T::construct_async(self.as_ref()).await {
Ok(x) => { Ok(x) => {
self.fill_placeholder::<T>(x); self.fill_placeholder::<T>(x);
Ok(()) Ok(())
@ -340,6 +341,6 @@ mod tests {
async fn obtain_impl() { async fn obtain_impl() {
let state = Aerosol::new(); let state = Aerosol::new();
state.init_async::<Arc<DummyImpl>>().await; state.init_async::<Arc<DummyImpl>>().await;
state.get_async::<Arc<dyn DummyTrait>>().await; state.try_get_async::<Arc<dyn DummyTrait>>().await.unwrap();
} }
} }

View file

@ -13,6 +13,7 @@
//! //!
//! Provides integrations with the `axum` web framework. See the `axum` module //! Provides integrations with the `axum` web framework. See the `axum` module
//! for more information. //! for more information.
pub use frunk;
#[cfg(feature = "async")] #[cfg(feature = "async")]
mod async_; mod async_;
@ -20,6 +21,7 @@ mod async_;
mod async_constructible; mod async_constructible;
#[cfg(feature = "axum")] #[cfg(feature = "axum")]
pub mod axum; pub mod axum;
mod macros;
mod resource; mod resource;
mod slot; mod slot;
mod state; mod state;

7
src/macros.rs Normal file
View file

@ -0,0 +1,7 @@
/// Define a custom `Aerosol` alias with a specific set of required types
#[macro_export]
macro_rules! Aero {
($($tok:tt)*) => {
$crate::Aerosol<$crate::frunk::HList![$($tok)*]>
};
}

View file

@ -1 +0,0 @@
fn main() {}

View file

@ -1,6 +1,11 @@
use std::{any::Any, sync::Arc, task::Poll}; use std::{any::Any, marker::PhantomData, sync::Arc, task::Poll};
use anymap::hashbrown::{Entry, Map}; use anymap::hashbrown::{Entry, Map};
use frunk::{
hlist::{HFoldRightable, Sculptor},
prelude::HList,
HCons, HNil, Poly,
};
use parking_lot::RwLock; use parking_lot::RwLock;
use crate::{ use crate::{
@ -16,16 +21,67 @@ struct InnerAerosol {
/// Stores a collection of resources keyed on resource type. /// Stores a collection of resources keyed on resource type.
/// Provides methods for accessing this collection. /// Provides methods for accessing this collection.
/// Can be cheaply cloned. /// Can be cheaply cloned.
#[derive(Debug, Clone, Default)] #[derive(Debug)]
pub struct Aerosol { #[repr(transparent)]
pub struct Aerosol<R: HList = HNil> {
inner: Arc<RwLock<InnerAerosol>>, inner: Arc<RwLock<InnerAerosol>>,
phantom: PhantomData<Arc<R>>,
} }
impl Aerosol { impl Aerosol {
/// Construct a new instance of the type with no initial resources. /// Construct a new instance of the type with no initial resources.
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self {
inner: Default::default(),
phantom: PhantomData,
}
} }
}
impl<R: HList> Clone for Aerosol<R> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
phantom: PhantomData,
}
}
}
struct AerosolBuilderFolder;
impl<R: HList, T: Resource> frunk::Func<(Aerosol<R>, T)> for AerosolBuilderFolder {
type Output = Aerosol<HCons<T, R>>;
fn call((aero, value): (Aerosol<R>, T)) -> Self::Output {
aero.with(value)
}
}
#[doc(hidden)]
pub trait HTestable {
fn test<R: HList>(aero: &Aerosol<R>) -> bool;
}
impl HTestable for HNil {
fn test<R: HList>(_aero: &Aerosol<R>) -> bool {
true
}
}
impl<H: Resource, T: HTestable> HTestable for HCons<H, T> {
fn test<R: HList>(aero: &Aerosol<R>) -> bool {
aero.has::<H>() && T::test(aero)
}
}
impl<
R: Default + HFoldRightable<Poly<AerosolBuilderFolder>, Aerosol, Output = Aerosol<R>> + HList,
> Default for Aerosol<R>
{
fn default() -> Self {
R::default().foldr(Poly(AerosolBuilderFolder), Aerosol::new())
}
}
impl<R: HList> Aerosol<R> {
/// Directly insert a resource into the collection. Panics if a resource of the /// Directly insert a resource into the collection. Panics if a resource of the
/// same type already exists. /// same type already exists.
pub fn insert<T: Resource>(&self, value: T) { pub fn insert<T: Resource>(&self, value: T) {
@ -38,10 +94,76 @@ impl Aerosol {
} }
/// Builder method equivalent to calling `insert()` but can be chained. /// Builder method equivalent to calling `insert()` but can be chained.
pub fn with<T: Resource>(self, value: T) -> Self { pub fn with<T: Resource>(self, value: T) -> Aerosol<HCons<T, R>> {
self.insert(value); self.insert(value);
self Aerosol {
inner: self.inner,
phantom: PhantomData,
}
} }
/// Convert into a different variant of the Aerosol type. The new variant must
/// not require any resources which are not required as part of this type.
pub fn into<R2: HList, I>(self) -> Aerosol<R2>
where
R: Sculptor<R2, I>,
{
Aerosol {
inner: self.inner,
phantom: PhantomData,
}
}
/// Reborrow as a different variant of the Aerosol 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<R2: HList, I>(&self) -> &Aerosol<R2>
where
R: Sculptor<R2, I>,
{
// Safety: all Aerosol 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
/// original type if one or more of the required resources are not fully
/// constructed.
pub fn try_into<R2: HList + HTestable>(self) -> Result<Aerosol<R2>, Self> {
if R2::test(&self) {
Ok(Aerosol {
inner: self.inner,
phantom: PhantomData,
})
} else {
Err(self)
}
}
/// Try to convert into a different variant of the Aerosol type. Returns
/// `None` if one or more of the required resources are not fully
/// constructed.
pub fn try_as_ref<R2: HList + HTestable>(&self) -> Option<&Aerosol<R2>> {
if R2::test(self) {
Some(
// Safety: all Aerosol variants are `#[repr(transparent)]` wrappers around
// the same concrete type.
unsafe { std::mem::transmute(self) },
)
} else {
None
}
}
/// Check if a resource with a specific type is fully constructed in this
/// aerosol instance
pub fn has<T: Resource>(&self) -> bool {
matches!(
self.inner.read().items.get::<Slot<T>>(),
Some(Slot::Filled(_))
)
}
pub(crate) fn try_get_slot<T: Resource>(&self) -> Option<SlotDesc<T>> { pub(crate) fn try_get_slot<T: Resource>(&self) -> Option<SlotDesc<T>> {
self.inner.read().items.get().map(Slot::desc) self.inner.read().items.get().map(Slot::desc)
} }
@ -89,8 +211,22 @@ impl Aerosol {
} }
} }
impl<R: HList> AsRef<Aerosol> for Aerosol<R> {
fn as_ref(&self) -> &Aerosol {
Aerosol::as_ref(self)
}
}
impl<H, T: HList> From<Aerosol<HCons<H, T>>> for Aerosol {
fn from(value: Aerosol<HCons<H, T>>) -> Self {
value.into()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Aero;
use super::*; use super::*;
#[test] #[test]
@ -105,4 +241,18 @@ mod tests {
let state = Aerosol::new().with(13); let state = Aerosol::new().with(13);
state.insert(42); state.insert(42);
} }
#[test]
fn default() {
let state: Aero![i32] = Aerosol::default();
state.insert("Hello, world!");
}
#[test]
fn convert() {
let state: Aero![i32, String, f32] = Aerosol::default();
state.insert("Hello, world!");
let state2: Aero![f32, String] = state.into();
let _state3: Aero![i32, String, f32] = state2.try_into().unwrap();
}
} }

View file

@ -1,5 +1,7 @@
use std::{task::Poll, thread}; use std::{task::Poll, thread};
use frunk::{hlist::Plucker, prelude::HList};
use crate::{ use crate::{
resource::{unwrap_resource, Resource}, resource::{unwrap_resource, Resource},
slot::SlotDesc, slot::SlotDesc,
@ -16,7 +18,7 @@ pub fn safe_park() {
std::thread::park(); std::thread::park();
} }
impl Aerosol { impl<R: HList> Aerosol<R> {
/// Synchronously wait for the slot for `T` to not have a placeholder. /// 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. /// 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> { pub(crate) fn wait_for_slot<T: Resource>(&self, insert_placeholder: bool) -> Option<T> {
@ -37,9 +39,11 @@ impl Aerosol {
SlotDesc::Placeholder => self.wait_for_slot::<T>(false), SlotDesc::Placeholder => self.wait_for_slot::<T>(false),
} }
} }
/// Get an instance of `T` from the AppState, and panic if not found. /// Get an instance of `T` from the AppState which is statically known to be present.
/// This function does not attempt to construct `T` if it does not exist. pub fn get<T: Resource, I>(&self) -> T
pub fn get<T: Resource>(&self) -> T { where
R: Plucker<T, I>,
{
unwrap_resource(self.try_get()) unwrap_resource(self.try_get())
} }
} }
@ -51,14 +55,7 @@ mod tests {
#[test] #[test]
fn get_with() { fn get_with() {
let state = Aerosol::new().with(42); let state = Aerosol::new().with(42);
assert_eq!(state.get::<i32>(), 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] #[test]

View file

@ -1,5 +1,7 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use frunk::prelude::HList;
use crate::{ use crate::{
resource::{unwrap_constructed, Resource}, resource::{unwrap_constructed, Resource},
slot::SlotDesc, slot::SlotDesc,
@ -90,14 +92,14 @@ impl_constructible! {
pub trait ConstructibleResource: Resource + IndirectlyConstructible {} pub trait ConstructibleResource: Resource + IndirectlyConstructible {}
impl<T: Resource + IndirectlyConstructible> ConstructibleResource for T {} impl<T: Resource + IndirectlyConstructible> ConstructibleResource for T {}
impl Aerosol { impl<R: HList> Aerosol<R> {
/// Try to get or construct an instance of `T`. /// Try to get or construct an instance of `T`.
pub fn try_obtain<T: ConstructibleResource>(&self) -> Result<T, T::Error> { pub fn try_obtain<T: ConstructibleResource>(&self) -> Result<T, T::Error> {
match self.try_get_slot() { match self.try_get_slot() {
Some(SlotDesc::Filled(x)) => Ok(x), Some(SlotDesc::Filled(x)) => Ok(x),
Some(SlotDesc::Placeholder) | None => match self.wait_for_slot::<T>(true) { Some(SlotDesc::Placeholder) | None => match self.wait_for_slot::<T>(true) {
Some(x) => Ok(x), Some(x) => Ok(x),
None => match T::construct(self) { None => match T::construct(self.as_ref()) {
Ok(x) => { Ok(x) => {
self.fill_placeholder::<T>(x.clone()); self.fill_placeholder::<T>(x.clone());
Ok(x) Ok(x)
@ -118,7 +120,7 @@ impl Aerosol {
pub fn try_init<T: ConstructibleResource>(&self) -> Result<(), T::Error> { pub fn try_init<T: ConstructibleResource>(&self) -> Result<(), T::Error> {
match self.wait_for_slot::<T>(true) { match self.wait_for_slot::<T>(true) {
Some(_) => Ok(()), Some(_) => Ok(()),
None => match T::construct(self) { None => match T::construct(self.as_ref()) {
Ok(x) => { Ok(x) => {
self.fill_placeholder::<T>(x); self.fill_placeholder::<T>(x);
Ok(()) Ok(())
@ -264,6 +266,6 @@ mod tests {
fn obtain_impl() { fn obtain_impl() {
let state = Aerosol::new(); let state = Aerosol::new();
state.init::<Arc<DummyImpl>>(); state.init::<Arc<DummyImpl>>();
state.get::<Arc<dyn DummyTrait>>(); state.try_get::<Arc<dyn DummyTrait>>().unwrap();
} }
} }