Skip to content

Commit

Permalink
feat(di): nested impl traits (#31)
Browse files Browse the repository at this point in the history
Sometimes an `impl Trait` can be more than one level of generics deep.
Or it might come from more than just the immediate children. This PR
handles these cases correctly.

It also expands the child dependency explicitly rather than calling a
function to get the child dependencies. This is needed to prevent
premature casting to `impl` types which leads to a compile error.

This also adds a complex example to show how dependency
container can be used with side-effects, mixed lifetimes, and async
dependencies.
  • Loading branch information
chesedo authored Sep 24, 2024
1 parent 39dcfef commit 9404817
Show file tree
Hide file tree
Showing 37 changed files with 838 additions and 506 deletions.
176 changes: 90 additions & 86 deletions despatma-dependency-container/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use quote::{quote, ToTokens};
use syn::{
parse_quote, parse_str,
punctuated::Punctuated,
token::{Async, Await, Fn, Paren},
Attribute, Block, Field, FieldValue, FieldsNamed, FnArg, Ident, Path, Signature, Token, Type,
token::{Async, Fn, Paren},
Attribute, Block, Field, FieldValue, FieldsNamed, FnArg, Ident, Path, Signature, Stmt, Token,
Type,
};

use crate::processing::{self, Lifetime};
Expand All @@ -27,7 +28,7 @@ pub struct Container {
dependencies: Vec<Dependency>,
}

#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
#[cfg_attr(test, derive(Eq, PartialEq, Debug, Clone))]
pub struct Dependency {
attrs: Vec<Attribute>,
block: Block,
Expand All @@ -39,13 +40,7 @@ pub struct Dependency {
ty: Type,
create_asyncness: Option<Async>,
is_managed: bool,
dependencies: Vec<ChildDependency>,
}

#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
pub struct ChildDependency {
ident: Ident,
awaitness: Option<Await>,
dependencies: Vec<Dependency>,
}

impl From<processing::Container> for Container {
Expand Down Expand Up @@ -116,7 +111,7 @@ fn get_struct_fields(
quote! { std::cell::OnceCell<#field_ty> }
}
}
Lifetime::Transient => {
Lifetime::Transient(_) => {
unreachable!("we filtered for only singleton and scoped dependencies")
}
};
Expand Down Expand Up @@ -175,7 +170,7 @@ fn get_new_scope_constructors(
let init = match dep_ref.lifetime {
Lifetime::Singleton(_) => quote! { self.#ident.clone() },
Lifetime::Scoped(_) => quote! { Default::default() },
Lifetime::Transient => {
Lifetime::Transient(_) => {
unreachable!("we filtered for only singleton and scoped dependencies")
}
};
Expand All @@ -198,7 +193,6 @@ impl From<processing::Dependency> for Dependency {
block,
is_async,
is_boxed: _,
has_explicit_lifetime: _,
lifetime,
ty,
field_ty: _,
Expand Down Expand Up @@ -233,7 +227,7 @@ impl From<processing::Dependency> for Dependency {

let dependencies = dependencies
.into_iter()
.map(ChildDependency::from)
.map(|d| d.inner.borrow().clone().into())
.collect();

Self {
Expand All @@ -252,20 +246,6 @@ impl From<processing::Dependency> for Dependency {
}
}

impl From<processing::ChildDependency> for ChildDependency {
fn from(child_dependency: processing::ChildDependency) -> Self {
let dep_ref = child_dependency.inner.borrow();
let ident = dep_ref.sig.ident.clone();
let awaitness = if dep_ref.is_async {
Some(<Token![await]>::default())
} else {
None
};

Self { ident, awaitness }
}
}

impl ToTokens for Container {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
Expand Down Expand Up @@ -309,16 +289,16 @@ impl ToTokens for Dependency {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
attrs,
block,
block: _,
asyncness,
fn_token,
ident,
paren_token,
inputs,
ty,
create_asyncness,
is_managed,
dependencies,
create_asyncness: _,
is_managed: _,
dependencies: _,
} = self;

// Do the same thing `syn` does for the paren_token
Expand All @@ -328,15 +308,49 @@ impl ToTokens for Dependency {
inputs.to_tokens(tokens);
});

let stmts = self.to_stmts();

tokens.extend(quote!(
#(#attrs)*
pub #asyncness #fn_token #ident(&'a self) -> #ty {
#(#stmts);*
}
));
}
}

impl Dependency {
fn to_stmts(&self) -> Vec<Stmt> {
let Self {
attrs: _,
block,
asyncness: _,
fn_token: _,
ident,
paren_token: _,
inputs: _,
ty: _,
create_asyncness,
is_managed,
dependencies,
} = self;

let create_dependencies: Vec<_> = dependencies
.iter()
.map(|child_dependency| {
let ChildDependency { ident, awaitness } = child_dependency;
let stmts = child_dependency.to_stmts();
let ident = &child_dependency.ident;

let awaitness = awaitness.map(|awaitness| quote! { .#awaitness });
let block = if stmts.len() == 1 {
let stmt = &stmts[0];

quote! { #stmt }
} else {
quote! { { #(#stmts);* } }
};

let create_stmt = quote! {
let #ident = self.#ident()#awaitness;
let #ident = #block;
};

create_stmt
Expand All @@ -347,27 +361,25 @@ impl ToTokens for Dependency {
let final_stmt = if *is_managed {
if create_asyncness.is_some() {
quote! {
self.#ident.get_or_init(async { #block }).await
self.#ident.get_or_init(async #block ).await
}
} else {
quote! {
self.#ident.get_or_init(|| #block)
}
}
} else {
let stmts = &block.stmts;
quote! {
#block
#(#stmts)*
}
};

tokens.extend(quote!(
#(#attrs)*
pub #asyncness #fn_token #ident(&self) -> #ty {
#(#create_dependencies)*
parse_quote! {
#(#create_dependencies)*

#final_stmt
}
));
#final_stmt
}
}
}

Expand All @@ -393,10 +405,9 @@ mod tests {
block: parse_quote!({ Config::new().await }),
is_async: true,
is_boxed: false,
has_explicit_lifetime: false,
lifetime: Lifetime::Singleton(Span::call_site()),
ty: parse_quote! { Config },
field_ty: Some(parse_quote! { Config }),
field_ty: parse_quote! { Config },
dependencies: vec![],
}));
let db = Rc::new(RefCell::new(processing::Dependency {
Expand All @@ -407,10 +418,9 @@ mod tests {
block: parse_quote!({ Sqlite::new(config.conn_str) }),
is_async: true,
is_boxed: false,
has_explicit_lifetime: false,
lifetime: Lifetime::Singleton(Span::call_site()),
ty: parse_quote! { Sqlite },
field_ty: Some(parse_quote! { Sqlite }),
field_ty: parse_quote! { Sqlite },
dependencies: vec![processing::ChildDependency {
inner: config.clone(),
ty: parse_quote!(&Config),
Expand All @@ -430,10 +440,9 @@ mod tests {
block: parse_quote!({ Service::new(db) }),
is_async: true,
is_boxed: false,
has_explicit_lifetime: false,
lifetime: Lifetime::Transient,
lifetime: Lifetime::Transient(None),
ty: parse_quote! { Service },
field_ty: None,
field_ty: parse_quote! { Service },
dependencies: vec![processing::ChildDependency {
inner: db,
ty: parse_quote!(&Sqlite),
Expand All @@ -452,42 +461,41 @@ mod tests {
};
named.named
};
let config = Dependency {
attrs: vec![],
block: parse_quote!({ Config::new().await }),
asyncness: Some(parse_quote!(async)),
fn_token: parse_quote!(fn),
ident: parse_quote!(config),
paren_token: Default::default(),
inputs: parse_quote!(&self),
ty: parse_quote!(&Config),
create_asyncness: Some(parse_quote!(async)),
is_managed: true,
dependencies: vec![],
};
let db = Dependency {
attrs: vec![],
block: parse_quote!({ Sqlite::new(config.conn_str) }),
asyncness: Some(parse_quote!(async)),
fn_token: parse_quote!(fn),
ident: parse_quote!(db),
paren_token: Default::default(),
inputs: parse_quote!(&self, config: &Config),
ty: parse_quote!(&Sqlite),
create_asyncness: None,
is_managed: true,
dependencies: vec![config.clone()],
};
let expected = super::Container {
attrs: vec![],
self_ty: parse_quote! { Container },
fields,
constructors: parse_quote!( config: Default::default(), db: Default::default(), ),
scope_constructors: parse_quote!( config: self.config.clone(), db: self.db.clone(), ),
dependencies: vec![
Dependency {
attrs: vec![],
block: parse_quote!({ Config::new().await }),
asyncness: Some(parse_quote!(async)),
fn_token: parse_quote!(fn),
ident: parse_quote!(config),
paren_token: Default::default(),
inputs: parse_quote!(&self),
ty: parse_quote!(&Config),
create_asyncness: Some(parse_quote!(async)),
is_managed: true,
dependencies: vec![],
},
Dependency {
attrs: vec![],
block: parse_quote!({ Sqlite::new(config.conn_str) }),
asyncness: Some(parse_quote!(async)),
fn_token: parse_quote!(fn),
ident: parse_quote!(db),
paren_token: Default::default(),
inputs: parse_quote!(&self, config: &Config),
ty: parse_quote!(&Sqlite),
create_asyncness: None,
is_managed: true,
dependencies: vec![ChildDependency {
ident: parse_quote!(config),
awaitness: Some(parse_quote!(await)),
}],
},
config,
db.clone(),
Dependency {
attrs: vec![],
block: parse_quote!({ Service::new(db) }),
Expand All @@ -499,10 +507,7 @@ mod tests {
ty: parse_quote!(Service),
create_asyncness: None,
is_managed: false,
dependencies: vec![ChildDependency {
ident: parse_quote!(db),
awaitness: Some(parse_quote!(await)),
}],
dependencies: vec![db],
},
],
};
Expand All @@ -520,10 +525,9 @@ mod tests {
block: parse_quote!({ Box::new(Sqlite::new()) }),
is_async: false,
is_boxed: true,
has_explicit_lifetime: false,
lifetime: Lifetime::Scoped(Span::call_site()),
ty: parse_quote! { std::boxed::Box<dyn DB + 'a> },
field_ty: Some(parse_quote! { std::boxed::Box<dyn DB + 'a> }),
field_ty: parse_quote! { std::boxed::Box<dyn DB + 'a> },
dependencies: vec![],
};
let dependency: Dependency = dependency.into();
Expand Down
Loading

0 comments on commit 9404817

Please sign in to comment.