Skip to content

Commit

Permalink
Add Lazy<T> spec and catalog scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiimk committed Dec 7, 2024
1 parent e7c1f99 commit 65c315f
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 25 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `Catalog::scope` and `Catalog::current` allow setting and accessing a "current" catalog in a task-local context
- Requires new `tokio` crate feature
- `Lazy<T>` injection spec that delays the creation of a value until it's requested
- Can be used to delay initialization of expensive values that are rarely used
- Can be used in combination with `Catalog::scope` to inject values registered dynamically

## [0.9.3] - 2024-12-06
### Changed
- Upgraded to `thiserror v2` dependency
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ assert_eq!(inst.test(), "aimpl::bimpl");
- Injection specs:
- `OneOf` - expects a single implementation of a given interface
- `AllOf` - returns a collection of all implementations on a given interface
- `Maybe<Spec>` - Returns `None` if inner `Spec` cannot be resolved
- `Maybe<Spec>` - returns `None` if inner `Spec` cannot be resolved
- `Lazy<Spec>` - injects an object that delays the creation of value until it is requested
- Component scopes:
- `Transient` (default) - a new instance is created for every invocation
- `Singleton` - an instance is created upon first use and then reused for the rest of calls
Expand All @@ -89,6 +90,7 @@ assert_eq!(inst.test(), "aimpl::bimpl");
- By value injection of `Clone` types
- `Catalog` can be self-injected
- Chaining of `Catalog`s allows adding values dynamically (e.g. in middleware chains like `tower`)
- `Catalog` can be scoped within a `tokio` task as "current" to override the source of `Lazy`ly injected values


# Design Principles
Expand Down Expand Up @@ -127,7 +129,6 @@ assert_eq!(inst.test(), "aimpl::bimpl");
- task
- catalog?
- thread safety
- lazy values
- externally defined types
- custom builders
- error handling
Expand Down
12 changes: 12 additions & 0 deletions dill-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,12 @@ fn implement_arg(
}
_ => unimplemented!("Currently only Option<Arc<Iface>> is supported"),
},
InjectionType::Lazy { element } => match element.as_ref() {
InjectionType::Arc { inner } => {
quote! { ::dill::specs::Lazy::<::dill::OneOf::<#inner>>::check(cat) }
}
_ => unimplemented!("Currently only Option<Arc<Iface>> is supported"),
},
InjectionType::Vec { item } => match item.as_ref() {
InjectionType::Arc { inner } => quote! { ::dill::AllOf::<#inner>::check(cat) },
_ => unimplemented!("Currently only Vec<Arc<Iface>> is supported"),
Expand All @@ -387,6 +393,12 @@ fn implement_arg(
}
_ => unimplemented!("Currently only Option<Arc<Iface>> is supported"),
},
InjectionType::Lazy { element } => match element.as_ref() {
InjectionType::Arc { inner } => {
quote! { ::dill::specs::Lazy::<::dill::OneOf::<#inner>>::get(cat)? }
}
_ => unimplemented!("Currently only Lazy<Arc<Iface>> is supported"),
},
InjectionType::Vec { item } => match item.as_ref() {
InjectionType::Arc { inner } => quote! { ::dill::AllOf::<#inner>::get(cat)? },
_ => unimplemented!("Currently only Vec<Arc<Iface>> is supported"),
Expand Down
34 changes: 34 additions & 0 deletions dill-impl/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) enum InjectionType {
Reference { inner: syn::Type },
Option { element: Box<InjectionType> },
Vec { item: Box<InjectionType> },
Lazy { element: Box<InjectionType> },
Value { typ: syn::Type },
}

Expand All @@ -27,6 +28,10 @@ pub(crate) fn deduce_injection_type(typ: &syn::Type) -> InjectionType {
InjectionType::Vec {
item: Box::new(deduce_injection_type(&get_vec_item_type(typ))),
}
} else if is_lazy(typ) {
InjectionType::Lazy {
element: Box::new(deduce_injection_type(&get_lazy_element_type(typ))),
}
} else {
InjectionType::Value { typ: typ.clone() }
}
Expand Down Expand Up @@ -107,6 +112,35 @@ pub(crate) fn get_option_element_type(typ: &syn::Type) -> syn::Type {

/////////////////////////////////////////////////////////////////////////////////////////

pub(crate) fn is_lazy(typ: &syn::Type) -> bool {
let syn::Type::Path(typepath) = typ else {
return false;
};

if typepath.qself.is_some() || typepath.path.segments.len() != 1 {
return false;
}

&typepath.path.segments[0].ident == "Lazy"
}

pub(crate) fn get_lazy_element_type(typ: &syn::Type) -> syn::Type {
let syn::Type::Path(typepath) = typ else {
panic!("Type is not an Option")
};

assert!(typepath.qself.is_none());
assert_eq!(typepath.path.segments.len(), 1);
assert_eq!(&typepath.path.segments[0].ident, "Lazy");

let syn::PathArguments::AngleBracketed(args) = &typepath.path.segments[0].arguments else {
panic!("No generic type specifier found in Lazy")
};
syn::parse2(args.args.to_token_stream()).unwrap()
}

/////////////////////////////////////////////////////////////////////////////////////////

pub(crate) fn is_vec(typ: &syn::Type) -> bool {
let syn::Type::Path(typepath) = typ else {
return false;
Expand Down
13 changes: 13 additions & 0 deletions dill/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ keywords = { workspace = true }
include = { workspace = true }
edition = { workspace = true }


[features]
default = []
tokio = ["dep:tokio"]


[dependencies]
dill-impl = { workspace = true }
thiserror = "2"
multimap = "0.10"

# Optional
tokio = { optional = true, version = "1", default-features = false }


[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
14 changes: 7 additions & 7 deletions dill/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,35 +235,35 @@ where

/////////////////////////////////////////////////////////////////////////////////////////

pub(crate) struct Lazy<Fct, Impl>
pub(crate) struct LazyBuilder<Fct, Impl>
where
Fct: FnOnce() -> Impl,
Impl: 'static + Send + Sync,
{
state: Mutex<LazyState<Fct, Impl>>,
state: Mutex<LazyBuilderState<Fct, Impl>>,
}

struct LazyState<Fct, Impl> {
struct LazyBuilderState<Fct, Impl> {
factory: Option<Fct>,
instance: Option<Arc<Impl>>,
}

impl<Fct, Impl> Lazy<Fct, Impl>
impl<Fct, Impl> LazyBuilder<Fct, Impl>
where
Fct: FnOnce() -> Impl,
Impl: 'static + Send + Sync,
{
pub fn new(factory: Fct) -> Self {
Self {
state: Mutex::new(LazyState {
state: Mutex::new(LazyBuilderState {
factory: Some(factory),
instance: None,
}),
}
}
}

impl<Fct, Impl> Builder for Lazy<Fct, Impl>
impl<Fct, Impl> Builder for LazyBuilder<Fct, Impl>
where
Fct: FnOnce() -> Impl + Send + Sync,
Impl: 'static + Send + Sync,
Expand All @@ -289,7 +289,7 @@ where
}
}

impl<Fct, Impl> TypedBuilder<Impl> for Lazy<Fct, Impl>
impl<Fct, Impl> TypedBuilder<Impl> for LazyBuilder<Fct, Impl>
where
Fct: FnOnce() -> Impl + Send + Sync,
Impl: 'static + Send + Sync,
Expand Down
68 changes: 68 additions & 0 deletions dill/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,72 @@ impl Catalog {
{
OneOf::<Iface>::get(self)
}

/// Sets this catalog as "current" in the async task scope for the duration
/// of the provided coroutine.
///
/// Most useful when used in combination with [`crate::lazy::Lazy`] and
/// [`Self::builder_chained()`] for dynamically registering additional
/// types.
///
/// Scopes can be nested - at the end of the inner scope the catalog from an
/// outer scope will be restored as "current".
///
/// ### Examples
///
/// ```
/// use dill::*;
/// use tokio::runtime::Runtime;
///
/// Runtime::new().unwrap().block_on(async {
/// let cat = Catalog::builder().add_value(String::from("test")).build();
///
/// cat.scope(async move {
/// let val = Catalog::current().get_one::<String>().unwrap();
/// assert_eq!(val.as_str(), "test");
/// }).await;
/// })
/// ```
#[cfg(feature = "tokio")]
pub async fn scope<F, R>(&self, f: F) -> R
where
F: std::future::Future<Output = R>,
{
CURRENT_CATALOG.scope(self.clone(), f).await
}

/// Allows accessing the catalog in the current [`Self::scope`].
///
/// Note that you should very rarely be using this method directly if at
/// all. Instead you should rely on [`crate::lazy::Lazy`] for
/// delayed injection from a current catalog.
///
/// ### Panics
///
/// Will panic if called from the outside of a [`Self::scope`].
///
/// ### Examples
///
/// ```
/// use dill::*;
/// use tokio::runtime::Runtime;
///
/// Runtime::new().unwrap().block_on(async {
/// let cat = Catalog::builder().add_value(String::from("test")).build();
///
/// cat.scope(async move {
/// let val = Catalog::current().get_one::<String>().unwrap();
/// assert_eq!(val.as_str(), "test");
/// }).await;
/// })
/// ```
#[cfg(feature = "tokio")]
pub fn current() -> Catalog {
CURRENT_CATALOG.get()
}
}

#[cfg(feature = "tokio")]
tokio::task_local! {
pub(crate) static CURRENT_CATALOG: Catalog;
}
2 changes: 1 addition & 1 deletion dill/src/catalog_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ impl CatalogBuilder {
Fct: FnOnce() -> Impl + Send + Sync + 'static,
Impl: Send + Sync + 'static,
{
self.add_builder(Lazy::new(factory));
self.add_builder(LazyBuilder::new(factory));
self
}

Expand Down
26 changes: 26 additions & 0 deletions dill/src/lazy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::sync::Arc;

use crate::InjectionError;

#[derive(Clone)]
pub struct Lazy<T> {
factory: Arc<dyn Fn() -> Result<T, InjectionError> + Send + Sync>,
}

impl<T> std::fmt::Debug for Lazy<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Lazy").finish_non_exhaustive()
}
}

impl<T> Lazy<T> {
pub fn new(f: impl Fn() -> Result<T, InjectionError> + Send + Sync + 'static) -> Self {
Self {
factory: Arc::new(f),
}
}

pub fn get(&self) -> Result<T, InjectionError> {
(self.factory)()
}
}
26 changes: 11 additions & 15 deletions dill/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,25 +164,21 @@
//! assert_eq!(inst.url(), "http://foo:8080");
//! ```
pub use dill_impl::*;

mod builder;
pub use builder::*;

mod catalog;
mod catalog_builder;
pub use catalog_builder::*;
mod errors;
mod lazy;
mod scopes;
pub mod specs;
mod typecast_builder;

mod catalog;
pub use builder::*;
pub use catalog::*;

mod errors;
pub use catalog_builder::*;
pub use dill_impl::*;
pub use errors::*;

mod specs;
pub use specs::*;

mod scopes;
pub use lazy::Lazy;
pub use scopes::*;

mod typecast_builder;
pub use specs::*;
pub use typecast_builder::*;
36 changes: 36 additions & 0 deletions dill/src/specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,39 @@ impl<Inner: DependencySpec> DependencySpec for Maybe<Inner> {
}
}
}

/////////////////////////////////////////////////////////////////////////////////////////
// Lazy
/////////////////////////////////////////////////////////////////////////////////////////

pub struct Lazy<Inner: DependencySpec> {
_dummy: PhantomData<Inner>,
}

impl<Inner: DependencySpec> DependencySpec for Lazy<Inner> {
type ReturnType = crate::lazy::Lazy<Inner::ReturnType>;

#[cfg(not(feature = "tokio"))]
fn get(cat: &Catalog) -> Result<Self::ReturnType, InjectionError> {
let cat = cat.clone();
Ok(crate::lazy::Lazy::new(move || Inner::get(&cat)))
}

#[cfg(feature = "tokio")]
fn get(cat: &Catalog) -> Result<Self::ReturnType, InjectionError> {
// Lazy<T> will store the clone of a catalog it was initially created with
// It will however first attempt to resolve a current catalog if scope feature
// is used and only use the former as a fallback.
let fallback_cat = cat.clone();
Ok(crate::lazy::Lazy::new(move || match crate::CURRENT_CATALOG
.try_with(|cat| Inner::get(cat))
{
Ok(v) => v,
Err(_) => Inner::get(&fallback_cat),
}))
}

fn check(cat: &Catalog) -> Result<(), InjectionError> {
Inner::check(cat)
}
}
Loading

0 comments on commit 65c315f

Please sign in to comment.