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 b327f57
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 96 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
130 changes: 59 additions & 71 deletions dill-impl/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ pub(crate) enum InjectionType {
Reference { inner: syn::Type },
Option { element: Box<InjectionType> },
Vec { item: Box<InjectionType> },
Lazy { element: Box<InjectionType> },
Value { typ: syn::Type },
}

pub(crate) fn deduce_injection_type(typ: &syn::Type) -> InjectionType {
if is_reference(typ) {
InjectionType::Reference {
inner: strip_reference(typ),
}
} else if is_smart_ptr(typ) {
InjectionType::Arc {
inner: strip_smart_ptr(typ),
}
} else if is_option(typ) {
if let Some(inner) = strip_reference(typ) {
InjectionType::Reference { inner }
} else if let Some(inner) = get_arc_element_type(typ) {
InjectionType::Arc { inner }
} else if let Some(elem_typ) = get_option_element_type(typ) {
InjectionType::Option {
element: Box::new(deduce_injection_type(&get_option_element_type(typ))),
element: Box::new(deduce_injection_type(&elem_typ)),
}
} else if is_vec(typ) {
} else if let Some(elem_typ) = get_vec_item_type(typ) {
InjectionType::Vec {
item: Box::new(deduce_injection_type(&get_vec_item_type(typ))),
item: Box::new(deduce_injection_type(&elem_typ)),
}
} else if let Some(elem_typ) = get_lazy_element_type(typ) {
InjectionType::Lazy {
element: Box::new(deduce_injection_type(&elem_typ)),
}
} else {
InjectionType::Value { typ: typ.clone() }
Expand All @@ -34,104 +35,91 @@ pub(crate) fn deduce_injection_type(typ: &syn::Type) -> InjectionType {

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

pub(crate) fn is_reference(typ: &syn::Type) -> bool {
matches!(typ, syn::Type::Reference(_))
}

pub(crate) fn strip_reference(typ: &syn::Type) -> syn::Type {
pub(crate) fn strip_reference(typ: &syn::Type) -> Option<syn::Type> {
match typ {
syn::Type::Reference(r) => r.elem.as_ref().clone(),
_ => typ.clone(),
syn::Type::Reference(r) => Some(r.elem.as_ref().clone()),
_ => None,
}
}

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

pub(crate) fn is_smart_ptr(typ: &syn::Type) -> bool {
pub(crate) fn get_arc_element_type(typ: &syn::Type) -> Option<syn::Type> {
let syn::Type::Path(typepath) = typ else {
return false;
panic!("Expected a Type::Path");
};

if typepath.qself.is_some() || typepath.path.segments.len() != 1 {
return false;
if typepath.qself.is_some() || typepath.path.segments.last().unwrap().ident != "Arc" {
return None;
}

&typepath.path.segments[0].ident == "Arc"
}
let syn::PathArguments::AngleBracketed(args) =
&typepath.path.segments.last().unwrap().arguments
else {
return None;
};

pub(crate) fn strip_smart_ptr(typ: &syn::Type) -> syn::Type {
match typ {
syn::Type::Path(typepath) if typepath.qself.is_none() => {
match typepath.path.segments.first() {
Some(seg) if &seg.ident == "Arc" => match seg.arguments {
syn::PathArguments::AngleBracketed(ref args) => {
syn::parse2(args.args.to_token_stream()).unwrap()
}
_ => typ.clone(),
},
_ => typ.clone(),
}
}
_ => typ.clone(),
}
Some(syn::parse2(args.args.to_token_stream()).unwrap())
}

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

pub(crate) fn is_option(typ: &syn::Type) -> bool {
pub(crate) fn get_option_element_type(typ: &syn::Type) -> Option<syn::Type> {
let syn::Type::Path(typepath) = typ else {
return false;
panic!("Expected a Type::Path");
};

if typepath.qself.is_some() || typepath.path.segments.len() != 1 {
return false;
if typepath.qself.is_some() || &typepath.path.segments.last().unwrap().ident != "Option" {
return None;
}

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

pub(crate) fn get_option_element_type(typ: &syn::Type) -> syn::Type {
let syn::Type::Path(typepath) = typ else {
panic!("Type is not an Option")
let syn::PathArguments::AngleBracketed(args) =
&typepath.path.segments.last().unwrap().arguments
else {
return None;
};

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

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

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

pub(crate) fn is_vec(typ: &syn::Type) -> bool {
pub(crate) fn get_lazy_element_type(typ: &syn::Type) -> Option<syn::Type> {
let syn::Type::Path(typepath) = typ else {
return false;
panic!("Expected a Type::Path");
};

if typepath.qself.is_some() || typepath.path.segments.len() != 1 {
return false;
if typepath.qself.is_some() || &typepath.path.segments.last().unwrap().ident != "Lazy" {
return None;
}

&typepath.path.segments[0].ident == "Vec"
let syn::PathArguments::AngleBracketed(args) =
&typepath.path.segments.last().unwrap().arguments
else {
return None;
};

Some(syn::parse2(args.args.to_token_stream()).unwrap())
}

pub(crate) fn get_vec_item_type(typ: &syn::Type) -> syn::Type {
/////////////////////////////////////////////////////////////////////////////////////////

pub(crate) fn get_vec_item_type(typ: &syn::Type) -> Option<syn::Type> {
let syn::Type::Path(typepath) = typ else {
panic!("Type is not a Vec")
panic!("Expected a Type::Path");
};

assert!(typepath.qself.is_none());
assert_eq!(typepath.path.segments.len(), 1);
assert_eq!(&typepath.path.segments[0].ident, "Vec");
if typepath.qself.is_some() || typepath.path.segments.last().unwrap().ident != "Vec" {
return None;
}

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

Some(syn::parse2(args.args.to_token_stream()).unwrap())
}

/////////////////////////////////////////////////////////////////////////////////////////
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
Loading

0 comments on commit b327f57

Please sign in to comment.