├── .cargo-rdme.toml ├── entrait_macros ├── entrait_syntax.md ├── README.md ├── Cargo.toml └── src │ ├── idents.rs │ ├── signature │ ├── mod.rs │ ├── future.rs │ ├── fn_params.rs │ ├── converter.rs │ └── lifetimes.rs │ ├── sub_attributes.rs │ ├── entrait_fn │ ├── input_attr.rs │ └── mod.rs │ ├── token_util.rs │ ├── entrait_trait │ ├── out_trait.rs │ ├── input_attr.rs │ └── mod.rs │ ├── entrait_impl │ ├── mod.rs │ └── input_attr.rs │ ├── lib.rs │ ├── opt.rs │ ├── trait_codegen.rs │ ├── fn_delegation_codegen.rs │ ├── attributes.rs │ ├── analyze_generics.rs │ ├── generics.rs │ └── input.rs ├── .gitignore ├── .gitmodules ├── test_all_stable.sh ├── tests └── it │ ├── main.rs │ ├── mockall.rs │ ├── delegation_modes.rs │ ├── dependency_inversion.rs │ ├── simple.rs │ └── unimock.rs ├── examples ├── async-graphql │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── axum │ ├── Cargo.toml │ └── src │ │ └── main.rs └── README.md ├── .github └── workflows │ └── rust.yml ├── LICENSE ├── Cargo.toml ├── CHANGELOG.md └── README.md /.cargo-rdme.toml: -------------------------------------------------------------------------------- 1 | heading-base-level = 1 2 | -------------------------------------------------------------------------------- /entrait_macros/entrait_syntax.md: -------------------------------------------------------------------------------- 1 | ## Syntax 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | Cargo.lock 3 | .vscode 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/rust-realworld-entrait"] 2 | path = examples/rust-realworld-entrait 3 | url = https://github.com/audunhalland/rust-realworld-entrait.git 4 | -------------------------------------------------------------------------------- /entrait_macros/README.md: -------------------------------------------------------------------------------- 1 | # entrait_macros 2 | 3 | Macros for use with `entrait`. This crate is not intended as a direct dependency; all macros are re-exported by `entrait`. 4 | -------------------------------------------------------------------------------- /test_all_stable.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -e 3 | set -x 4 | 5 | cargo hack --feature-powerset --exclude-features "default" --exclude-no-default-features test 6 | cargo test --workspace --features "unimock" 7 | cargo test --doc --features "unimock" 8 | -------------------------------------------------------------------------------- /tests/it/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused)] 3 | #![allow(clippy::disallowed_names)] 4 | 5 | mod delegation_modes; 6 | mod dependency_inversion; 7 | mod mockall; 8 | mod simple; 9 | 10 | #[cfg(feature = "unimock")] 11 | mod unimock; 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /examples/async-graphql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-async-graphql" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | entrait = { path = "../../", features = ["unimock"] } 9 | async-graphql = "7" 10 | tokio = { version = "1", features = ["full"] } 11 | async-trait = "0.1" 12 | implementation = "0.1" 13 | 14 | [dev-dependencies] 15 | tower = "0.5" 16 | tower-http = { version = "0.6", features = ["trace"] } 17 | hyper = { version = "1", features = ["full"] } 18 | serde_json = "1" 19 | unimock = "0.6.2" 20 | -------------------------------------------------------------------------------- /entrait_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "entrait_macros" 3 | version = "0.7.1" 4 | authors = ["Audun Halland "] 5 | edition = "2021" 6 | rust-version = "1.60" 7 | license = "MIT" 8 | description = "Procedural macros reexported by entrait" 9 | repository = "https://github.com/audunhalland/entrait/" 10 | keywords = ["macro"] 11 | categories = ["rust-patterns"] 12 | 13 | [dependencies] 14 | syn = { version = "2.0.8", features = ["full", "visit-mut"] } 15 | quote = "1" 16 | proc-macro2 = "1" 17 | 18 | [lib] 19 | proc-macro = true 20 | -------------------------------------------------------------------------------- /examples/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-axum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | entrait = { path = "../../", features = ["unimock"] } 9 | axum = "0.7" 10 | tokio = { version = "1", features = ["full"] } 11 | serde = { version = "1", features = ["derive"] } 12 | implementation = "0.1" 13 | async-trait = "0.1" 14 | 15 | [dev-dependencies] 16 | tower = "0.5" 17 | tower-http = { version = "0.6", features = ["trace"] } 18 | hyper = { version = "1", features = ["full"] } 19 | serde_json = "1" 20 | unimock = "0.6.2" 21 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory lists examples of entrait used in combination with other 4 | Rust frameworks. 5 | 6 | If you miss some particular example, then please tell me by filing an issue. 7 | PRs are also very welcome! 8 | 9 | ## Frameworks lacking support for generics 10 | These frameworks have limited support for generics, and seem to have a harder time supporting inversion of control: 11 | 12 | * `rocket.rs`: [#408](https://github.com/SergioBenitez/Rocket/issues/408) 13 | * `actix-web`: It'll work, but not in a very idiomatic way. E.g. macros are out: https://stackoverflow.com/a/65646165 14 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: taiki-e/install-action@cargo-hack 17 | - name: Test feature powerset 18 | run: cargo hack --feature-powerset --exclude-features "default" --exclude-no-default-features test 19 | - name: Test workspace 20 | run: cargo test --workspace --features "unimock" 21 | - name: Doctest 22 | run: cargo test --doc --features "unimock" 23 | - name: Clippy 24 | run: cargo clippy --features "unimock" -- -D warnings 25 | - name: Build examples 26 | run: cargo build --all 27 | -------------------------------------------------------------------------------- /tests/it/mockall.rs: -------------------------------------------------------------------------------- 1 | mod basic { 2 | use entrait::*; 3 | 4 | #[entrait(MockallFoo, mockall)] 5 | fn mockall_foo(_deps: &(), arg: i32) -> i32 { 6 | arg 7 | } 8 | 9 | fn takes_foo(foo: &impl MockallFoo, arg: i32) -> i32 { 10 | foo.mockall_foo(arg) 11 | } 12 | 13 | #[test] 14 | fn test() { 15 | let mut mock = MockMockallFoo::new(); 16 | mock.expect_mockall_foo().return_const(42); 17 | 18 | let result = takes_foo(&mock, 1337); 19 | 20 | assert_eq!(42, result); 21 | } 22 | } 23 | 24 | mod entrait_for_trait { 25 | use entrait::*; 26 | 27 | #[entrait(mockall)] 28 | trait Trait { 29 | fn method(&self) -> i32; 30 | } 31 | 32 | #[test] 33 | fn test() { 34 | let mut mock = MockTrait::new(); 35 | mock.expect_method().return_const(42); 36 | 37 | assert_eq!(42, mock.method()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /entrait_macros/src/idents.rs: -------------------------------------------------------------------------------- 1 | pub struct CrateIdents { 2 | pub entrait: syn::Ident, 3 | pub core: syn::Ident, 4 | pub __unimock: syn::Ident, 5 | pub unimock: syn::Ident, 6 | } 7 | 8 | impl CrateIdents { 9 | pub fn new(span: proc_macro2::Span) -> Self { 10 | Self { 11 | entrait: syn::Ident::new("entrait", span), 12 | core: syn::Ident::new("core", span), 13 | __unimock: syn::Ident::new("__unimock", span), 14 | unimock: syn::Ident::new("unimock", span), 15 | } 16 | } 17 | } 18 | 19 | pub struct GenericIdents<'c> { 20 | pub crate_idents: &'c CrateIdents, 21 | 22 | /// "Impl" 23 | pub impl_self: syn::Ident, 24 | 25 | /// The "T" in `Impl` 26 | pub impl_t: syn::Ident, 27 | } 28 | 29 | impl<'c> GenericIdents<'c> { 30 | pub fn new(crate_idents: &'c CrateIdents, span: proc_macro2::Span) -> Self { 31 | Self { 32 | crate_idents, 33 | impl_self: syn::Ident::new("Impl", span), 34 | impl_t: syn::Ident::new("EntraitT", span), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Audun Halland 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "entrait" 3 | version = "0.7.1" 4 | authors = ["Audun Halland "] 5 | edition = "2021" 6 | rust-version = "1.75" 7 | license = "MIT" 8 | description = "Loosely coupled Rust application design made easy" 9 | repository = "https://github.com/audunhalland/entrait/" 10 | keywords = ["pattern", "ioc", "inversion-of-control", "di", "macro"] 11 | categories = ["rust-patterns", "development-tools::testing"] 12 | 13 | [features] 14 | default = [] 15 | unimock = ["dep:unimock"] 16 | 17 | [dependencies] 18 | entrait_macros = { path = "entrait_macros", version = "0.7.1" } 19 | implementation = "0.1" 20 | unimock = { version = "0.6.2", optional = true } 21 | 22 | [dev-dependencies] 23 | tokio = { version = "1", features = ["macros", "rt"] } 24 | feignhttp = "0.5" 25 | mockall = "0.12" 26 | tracing = "0.1" 27 | async-trait = "0.1" 28 | 29 | [lib] 30 | # do not run doctest by default with `cargo hack`. They are tested with a separate `cargo test --doc` run. 31 | doctest = false 32 | 33 | [package.metadata.docs.rs] 34 | features = ["unimock"] 35 | 36 | [workspace] 37 | members = [ 38 | "entrait_macros", 39 | "examples/async-graphql", 40 | "examples/axum" 41 | ] 42 | -------------------------------------------------------------------------------- /entrait_macros/src/signature/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod converter; 2 | 3 | mod fn_params; 4 | 5 | use std::ops::Deref; 6 | 7 | #[derive(Clone, Copy)] 8 | pub struct InputSig<'s> { 9 | sig: &'s syn::Signature, 10 | } 11 | 12 | impl<'s> InputSig<'s> { 13 | pub fn new(sig: &'s syn::Signature) -> Self { 14 | Self { sig } 15 | } 16 | } 17 | 18 | impl<'s> Deref for InputSig<'s> { 19 | type Target = &'s syn::Signature; 20 | 21 | fn deref(&self) -> &Self::Target { 22 | &self.sig 23 | } 24 | } 25 | 26 | pub enum ImplReceiverKind { 27 | // (&self, ..) 28 | SelfRef, 29 | // (&__impl, ..) 30 | StaticImpl, 31 | // (&self, &__impl, ..) 32 | DynamicImpl, 33 | } 34 | 35 | /// The fn signature inside the trait 36 | #[derive(Clone)] 37 | pub struct EntraitSignature { 38 | pub sig: syn::Signature, 39 | #[expect(unused)] 40 | pub et_lifetimes: Vec, 41 | } 42 | 43 | impl EntraitSignature { 44 | pub fn new(sig: syn::Signature) -> Self { 45 | Self { 46 | sig, 47 | et_lifetimes: vec![], 48 | } 49 | } 50 | } 51 | 52 | /// Only used for associated future: 53 | #[derive(Clone)] 54 | pub struct EntraitLifetime { 55 | #[expect(unused)] 56 | pub lifetime: syn::Lifetime, 57 | } 58 | 59 | #[derive(Clone, Copy)] 60 | pub enum ReceiverGeneration { 61 | Insert, 62 | Rewrite, 63 | None, 64 | } 65 | -------------------------------------------------------------------------------- /entrait_macros/src/sub_attributes.rs: -------------------------------------------------------------------------------- 1 | //! The other attributes specified _under_ entrait 2 | 3 | use quote::ToTokens; 4 | 5 | #[derive(Clone, Copy)] 6 | pub enum SubAttribute<'t> { 7 | AsyncTrait(&'t syn::Attribute), 8 | Other(&'t syn::Attribute), 9 | Automock(&'t syn::Attribute), 10 | } 11 | 12 | pub fn analyze_sub_attributes(attributes: &[syn::Attribute]) -> Vec> { 13 | attributes 14 | .iter() 15 | .map(|attribute| { 16 | let last_segment = attribute.path().segments.last(); 17 | let ident = last_segment.map(|segment| segment.ident.to_string()); 18 | 19 | if let Some(ident) = ident { 20 | match ident.as_str() { 21 | "async_trait" => SubAttribute::AsyncTrait(attribute), 22 | "automock" => SubAttribute::Automock(attribute), 23 | _ => SubAttribute::Other(attribute), 24 | } 25 | } else { 26 | SubAttribute::Other(attribute) 27 | } 28 | }) 29 | .collect() 30 | } 31 | 32 | pub fn contains_async_trait(sub_attributes: &[SubAttribute]) -> bool { 33 | sub_attributes 34 | .iter() 35 | .any(|sub_attributes| matches!(sub_attributes, SubAttribute::AsyncTrait(_))) 36 | } 37 | 38 | impl ToTokens for SubAttribute<'_> { 39 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 40 | match self { 41 | Self::AsyncTrait(attr) => attr.to_tokens(tokens), 42 | Self::Automock(attr) => attr.to_tokens(tokens), 43 | Self::Other(attr) => attr.to_tokens(tokens), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/it/delegation_modes.rs: -------------------------------------------------------------------------------- 1 | fn assert_is_send(_: &T) {} 2 | fn assert_is_sync(_: &T) {} 3 | 4 | mod borrow_dyn_sync { 5 | use super::*; 6 | use entrait::*; 7 | 8 | #[entrait(Foo)] 9 | fn foo(deps: &impl Bar) { 10 | deps.bar(); 11 | } 12 | 13 | #[entrait(delegate_by=ref)] 14 | trait Bar: 'static { 15 | fn bar(&self); 16 | } 17 | 18 | struct App(Box); 19 | 20 | impl AsRef for App { 21 | fn as_ref(&self) -> &dyn Bar { 22 | self.0.as_ref() 23 | } 24 | } 25 | 26 | struct Baz; 27 | 28 | impl Bar for Baz { 29 | fn bar(&self) {} 30 | } 31 | 32 | #[test] 33 | fn test_impl_borrow() { 34 | let app = Impl::new(App(Box::new(Baz))); 35 | 36 | assert_is_sync(&app); 37 | 38 | app.foo(); 39 | } 40 | } 41 | 42 | mod borrow_dyn_with_async_trait { 43 | use super::*; 44 | use async_trait::*; 45 | use entrait::*; 46 | 47 | #[entrait(Foo)] 48 | async fn foo(deps: &impl Bar) { 49 | deps.bar().await; 50 | } 51 | 52 | #[entrait(delegate_by=ref)] 53 | #[async_trait] 54 | trait Bar: Sync + 'static { 55 | async fn bar(&self); 56 | } 57 | 58 | struct Baz; 59 | 60 | struct App(Baz); 61 | 62 | impl AsRef for App { 63 | fn as_ref(&self) -> &dyn Bar { 64 | &self.0 65 | } 66 | } 67 | 68 | #[async_trait] 69 | impl Bar for Baz { 70 | async fn bar(&self) {} 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_async_borrow() { 75 | let app = Impl::new(App(Baz)); 76 | 77 | assert_is_send(&app); 78 | assert_is_sync(&app); 79 | 80 | app.foo().await; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_fn/input_attr.rs: -------------------------------------------------------------------------------- 1 | use crate::idents::CrateIdents; 2 | use crate::opt::*; 3 | 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | /// The `entrait` invocation for functions 7 | pub struct EntraitFnAttr { 8 | pub trait_visibility: syn::Visibility, 9 | pub trait_ident: syn::Ident, 10 | pub opts: Opts, 11 | 12 | pub crate_idents: CrateIdents, 13 | } 14 | 15 | impl Parse for EntraitFnAttr { 16 | fn parse(input: ParseStream) -> syn::Result { 17 | let span = input.span(); 18 | let trait_visibility: syn::Visibility = input.parse()?; 19 | 20 | let trait_ident: syn::Ident = input.parse()?; 21 | 22 | let mut no_deps = None; 23 | let mut debug = None; 24 | let mut export = None; 25 | let mut future_send = None; 26 | let mut mock_api = None; 27 | let mut unimock = None; 28 | let mut mockall = None; 29 | 30 | while input.peek(syn::token::Comma) { 31 | input.parse::()?; 32 | 33 | match input.parse::()? { 34 | EntraitOpt::NoDeps(opt) => no_deps = Some(opt), 35 | EntraitOpt::Debug(opt) => debug = Some(opt), 36 | EntraitOpt::Export(opt) => export = Some(opt), 37 | EntraitOpt::MaybeSend(send) => future_send = Some(send), 38 | EntraitOpt::MockApi(ident) => mock_api = Some(ident), 39 | EntraitOpt::Unimock(opt) => unimock = Some(opt), 40 | EntraitOpt::Mockall(opt) => mockall = Some(opt), 41 | opt => return Err(syn::Error::new(opt.span(), "Unsupported option")), 42 | }; 43 | } 44 | 45 | let default_span = trait_ident.span(); 46 | 47 | Ok(EntraitFnAttr { 48 | trait_visibility, 49 | trait_ident, 50 | opts: Opts { 51 | default_span, 52 | no_deps, 53 | debug, 54 | export, 55 | future_send, 56 | mock_api, 57 | unimock, 58 | mockall, 59 | }, 60 | crate_idents: CrateIdents::new(span), 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/async-graphql/src/main.rs: -------------------------------------------------------------------------------- 1 | mod db { 2 | use entrait::*; 3 | 4 | #[entrait(pub FetchSomeValue, no_deps, mock_api=FetchSomeValueMock)] 5 | async fn fetch_some_value() -> String { 6 | "real".to_string() 7 | } 8 | } 9 | 10 | mod graphql { 11 | use super::db; 12 | use std::marker::PhantomData; 13 | 14 | pub struct Query(PhantomData); 15 | 16 | #[async_graphql::Object] 17 | impl Query 18 | where 19 | A: db::FetchSomeValue + Send + Sync + 'static, 20 | { 21 | async fn some_value(&self, ctx: &async_graphql::Context<'_>) -> Result { 22 | let app = ctx.data_unchecked::(); 23 | Ok(app.fetch_some_value().await) 24 | } 25 | } 26 | 27 | #[tokio::test] 28 | async fn unit_test_query() { 29 | use crate::db::FetchSomeValueMock; 30 | 31 | use async_graphql::*; 32 | use unimock::*; 33 | 34 | let deps = Unimock::new( 35 | FetchSomeValueMock 36 | .each_call(matching!()) 37 | .returns("mocked".to_string()), 38 | ); 39 | 40 | let response = async_graphql::Schema::build( 41 | Query::(PhantomData), 42 | EmptyMutation, 43 | EmptySubscription, 44 | ) 45 | .data(deps.clone()) 46 | .finish() 47 | .execute("{ someValue }") 48 | .await; 49 | 50 | assert_eq!( 51 | response.data, 52 | value!({ 53 | "someValue": "mocked" 54 | }) 55 | ); 56 | } 57 | 58 | #[tokio::test] 59 | async fn integration_test_query() { 60 | use async_graphql::*; 61 | use entrait::Impl; 62 | 63 | let app = Impl::new(()); 64 | let response = async_graphql::Schema::build( 65 | Query::>(PhantomData), 66 | EmptyMutation, 67 | EmptySubscription, 68 | ) 69 | .data(app) 70 | .finish() 71 | .execute("{ someValue }") 72 | .await; 73 | 74 | assert_eq!( 75 | response.data, 76 | value!({ 77 | "someValue": "real" 78 | }) 79 | ); 80 | } 81 | } 82 | 83 | fn main() {} 84 | -------------------------------------------------------------------------------- /entrait_macros/src/token_util.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::ToTokens; 3 | 4 | macro_rules! push_tokens { 5 | ($stream:expr, $token:expr) => { 6 | $token.to_tokens($stream) 7 | }; 8 | ($stream:expr, $token:expr, $($rest:expr),+) => { 9 | $token.to_tokens($stream); 10 | push_tokens!($stream, $($rest),*) 11 | }; 12 | } 13 | 14 | pub(crate) use push_tokens; 15 | 16 | pub struct TokenPair(pub T, pub U); 17 | 18 | impl quote::ToTokens for TokenPair { 19 | fn to_tokens(&self, stream: &mut TokenStream) { 20 | push_tokens!(stream, self.0, self.1); 21 | } 22 | } 23 | 24 | pub struct EmptyToken; 25 | 26 | impl quote::ToTokens for EmptyToken { 27 | fn to_tokens(&self, _: &mut TokenStream) {} 28 | } 29 | 30 | pub struct Punctuator<'s, S, P, E: ToTokens> { 31 | stream: &'s mut TokenStream, 32 | position: usize, 33 | start: S, 34 | punct: P, 35 | end: E, 36 | } 37 | 38 | pub fn comma_sep( 39 | stream: &mut TokenStream, 40 | span: proc_macro2::Span, 41 | ) -> Punctuator { 42 | Punctuator::new(stream, EmptyToken, syn::token::Comma(span), EmptyToken) 43 | } 44 | 45 | impl<'s, S, P, E> Punctuator<'s, S, P, E> 46 | where 47 | S: quote::ToTokens, 48 | P: quote::ToTokens, 49 | E: quote::ToTokens, 50 | { 51 | pub fn new(stream: &'s mut TokenStream, start: S, punct: P, end: E) -> Self { 52 | Self { 53 | stream, 54 | position: 0, 55 | start, 56 | punct, 57 | end, 58 | } 59 | } 60 | 61 | pub fn push(&mut self, tokens: T) { 62 | self.sep(); 63 | tokens.to_tokens(self.stream); 64 | } 65 | 66 | pub fn push_fn(&mut self, f: F) 67 | where 68 | F: FnOnce(&mut TokenStream), 69 | { 70 | self.sep(); 71 | f(self.stream); 72 | } 73 | 74 | fn sep(&mut self) { 75 | if self.position == 0 { 76 | self.start.to_tokens(self.stream); 77 | } else { 78 | self.punct.to_tokens(self.stream); 79 | } 80 | 81 | self.position += 1; 82 | } 83 | } 84 | 85 | impl Drop for Punctuator<'_, S, P, E> 86 | where 87 | E: quote::ToTokens, 88 | { 89 | fn drop(&mut self) { 90 | if self.position > 0 { 91 | self.end.to_tokens(self.stream); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::disallowed_names)] 2 | 3 | use entrait::*; 4 | 5 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 6 | pub struct Foo { 7 | value: String, 8 | } 9 | 10 | /// "Business logic" 11 | mod business { 12 | use super::*; 13 | 14 | #[entrait(pub GetFoo, no_deps, mock_api=GetFooMock)] 15 | async fn get_foo() -> Foo { 16 | Foo { 17 | value: "real".to_string(), 18 | } 19 | } 20 | } 21 | 22 | /// Axum specific 23 | mod rest { 24 | use super::*; 25 | use axum::extract::Extension; 26 | use axum::routing::get; 27 | use axum::Json; 28 | 29 | pub struct Routes(std::marker::PhantomData); 30 | 31 | impl Routes 32 | where 33 | A: business::GetFoo + Send + Sync + Sized + Clone + 'static, 34 | { 35 | pub fn router() -> axum::Router { 36 | axum::Router::new().route("/foo", get(Self::get_foo)) 37 | } 38 | 39 | async fn get_foo(Extension(app): Extension) -> Json { 40 | Json(app.get_foo().await) 41 | } 42 | } 43 | 44 | #[tokio::test] 45 | async fn unit_test_router() { 46 | use axum::http::Request; 47 | use tower::ServiceExt; 48 | use unimock::*; 49 | 50 | let deps = Unimock::new(business::GetFooMock.each_call(matching!()).returns(Foo { 51 | value: "mocked".to_string(), 52 | })); 53 | let router = Routes::::router().layer(Extension(deps.clone())); 54 | let response = router 55 | .oneshot( 56 | Request::get("/foo") 57 | .body(axum::body::Body::empty()) 58 | .unwrap(), 59 | ) 60 | .await 61 | .unwrap(); 62 | let bytes = axum::body::to_bytes(response.into_body(), 1_000_000) 63 | .await 64 | .unwrap(); 65 | let foo: Foo = serde_json::from_slice(&bytes).unwrap(); 66 | 67 | assert_eq!("mocked", foo.value); 68 | } 69 | } 70 | 71 | #[tokio::main] 72 | async fn main() { 73 | use axum::extract::Extension; 74 | use entrait::Impl; 75 | 76 | #[derive(Clone)] 77 | struct App; 78 | 79 | let app = Impl::new(App); 80 | let router = rest::Routes::>::router().layer(Extension(app)); 81 | 82 | let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); 83 | 84 | axum::serve(listener, router).await.unwrap(); 85 | } 86 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_trait/out_trait.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | analyze_generics::TraitFn, 3 | generics::{FnDeps, TraitGenerics}, 4 | signature::EntraitSignature, 5 | trait_codegen::{self, Supertraits}, 6 | }; 7 | 8 | use syn::spanned::Spanned; 9 | 10 | #[derive(Clone)] 11 | pub struct OutTrait { 12 | pub attrs: Vec, 13 | pub vis: syn::Visibility, 14 | #[expect(unused)] 15 | pub trait_token: syn::token::Trait, 16 | pub generics: TraitGenerics, 17 | pub ident: syn::Ident, 18 | pub supertraits: trait_codegen::Supertraits, 19 | pub fns: Vec, 20 | } 21 | 22 | pub fn analyze_trait(item_trait: syn::ItemTrait) -> syn::Result { 23 | let mut associated_types = vec![]; 24 | let mut fns = vec![]; 25 | 26 | for item in item_trait.items.into_iter() { 27 | match item { 28 | syn::TraitItem::Fn(method) => { 29 | let originally_async = method.sig.asyncness.is_some(); 30 | 31 | let entrait_sig = EntraitSignature::new(method.sig); 32 | 33 | fns.push(TraitFn { 34 | deps: FnDeps::NoDeps, 35 | attrs: method.attrs, 36 | entrait_sig, 37 | originally_async, 38 | }); 39 | } 40 | syn::TraitItem::Type(ty) => { 41 | associated_types.push(ty); 42 | } 43 | item => { 44 | return Err(syn::Error::new( 45 | item.span(), 46 | "Entrait does not support this kind of trait item.", 47 | )); 48 | } 49 | } 50 | } 51 | 52 | let supertraits = if let Some(colon_token) = item_trait.colon_token { 53 | Supertraits::Some { 54 | colon_token, 55 | bounds: item_trait.supertraits, 56 | } 57 | } else { 58 | Supertraits::None 59 | }; 60 | 61 | Ok(OutTrait { 62 | attrs: item_trait.attrs, 63 | vis: item_trait.vis, 64 | trait_token: item_trait.trait_token, 65 | ident: item_trait.ident, 66 | generics: TraitGenerics { 67 | params: item_trait.generics.params, 68 | where_predicates: item_trait 69 | .generics 70 | .where_clause 71 | .map(|where_clause| where_clause.predicates) 72 | .unwrap_or_default(), 73 | }, 74 | supertraits, 75 | fns, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_trait/input_attr.rs: -------------------------------------------------------------------------------- 1 | use crate::idents::CrateIdents; 2 | use crate::opt::*; 3 | 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | pub struct EntraitTraitAttr { 7 | pub impl_trait: Option, 8 | pub opts: Opts, 9 | pub delegation_kind: Option>, 10 | pub crate_idents: CrateIdents, 11 | } 12 | 13 | #[expect(unused)] 14 | pub struct ImplTrait(pub syn::Visibility, pub syn::Ident); 15 | 16 | impl Parse for EntraitTraitAttr { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | let span = input.span(); 19 | 20 | let mut impl_trait = None; 21 | 22 | if !input.is_empty() && input.fork().parse::().is_err() { 23 | let vis: syn::Visibility = input.parse()?; 24 | let ident: syn::Ident = input.parse()?; 25 | 26 | impl_trait = Some(ImplTrait(vis, ident)); 27 | 28 | if input.peek(syn::token::Comma) { 29 | input.parse::()?; 30 | } 31 | } 32 | 33 | let mut debug = None; 34 | let mut mock_api = None; 35 | let mut future_send = None; 36 | let mut unimock = None; 37 | let mut mockall = None; 38 | let mut delegation_kind = None; 39 | 40 | if !input.is_empty() { 41 | loop { 42 | match input.parse::()? { 43 | EntraitOpt::Debug(opt) => debug = Some(opt), 44 | EntraitOpt::MockApi(ident) => mock_api = Some(ident), 45 | EntraitOpt::MaybeSend(send) => future_send = Some(send), 46 | EntraitOpt::Unimock(opt) => unimock = Some(opt), 47 | EntraitOpt::Mockall(opt) => mockall = Some(opt), 48 | EntraitOpt::DelegateBy(kind) => delegation_kind = Some(kind), 49 | entrait_opt => { 50 | return Err(syn::Error::new(entrait_opt.span(), "Unsupported option")) 51 | } 52 | }; 53 | 54 | if input.peek(syn::token::Comma) { 55 | input.parse::()?; 56 | } else { 57 | break; 58 | } 59 | } 60 | } 61 | 62 | Ok(Self { 63 | impl_trait, 64 | opts: Opts { 65 | default_span: proc_macro2::Span::call_site(), 66 | no_deps: None, 67 | debug, 68 | export: None, 69 | future_send, 70 | mock_api, 71 | unimock, 72 | mockall, 73 | }, 74 | delegation_kind, 75 | crate_idents: CrateIdents::new(span), 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_impl/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod input_attr; 2 | 3 | use crate::analyze_generics; 4 | use crate::analyze_generics::detect_trait_dependency_mode; 5 | use crate::analyze_generics::TraitFnAnalyzer; 6 | use crate::fn_delegation_codegen; 7 | use crate::generics; 8 | use crate::input::ImplItem; 9 | use crate::input::InputImpl; 10 | use crate::signature; 11 | use crate::sub_attributes::analyze_sub_attributes; 12 | use crate::sub_attributes::SubAttribute; 13 | 14 | use quote::quote; 15 | use syn::spanned::Spanned; 16 | 17 | use self::input_attr::EntraitSimpleImplAttr; 18 | use self::input_attr::ImplKind; 19 | 20 | pub fn output_tokens_for_impl( 21 | attr: EntraitSimpleImplAttr, 22 | InputImpl { 23 | attrs, 24 | unsafety, 25 | impl_token, 26 | trait_path, 27 | for_token: _, 28 | self_ty, 29 | brace_token: _, 30 | items, 31 | }: InputImpl, 32 | ) -> syn::Result { 33 | let trait_span = trait_path 34 | .segments 35 | .last() 36 | .map(|segment| segment.span()) 37 | .unwrap_or_else(proc_macro2::Span::call_site); 38 | 39 | let mut generics_analyzer = analyze_generics::GenericsAnalyzer::new(); 40 | let trait_fns = items 41 | .iter() 42 | .filter_map(ImplItem::filter_fn) 43 | .map(|input_fn| { 44 | TraitFnAnalyzer { 45 | impl_receiver_kind: match attr.impl_kind { 46 | ImplKind::Static => signature::ImplReceiverKind::StaticImpl, 47 | ImplKind::DynRef => signature::ImplReceiverKind::DynamicImpl, 48 | }, 49 | trait_span, 50 | crate_idents: &attr.crate_idents, 51 | opts: &attr.opts, 52 | } 53 | .analyze(input_fn.input_sig(), &mut generics_analyzer) 54 | }) 55 | .collect::>>()?; 56 | let sub_attributes = analyze_sub_attributes(&attrs); 57 | 58 | let trait_generics = generics_analyzer.into_trait_generics(); 59 | 60 | let fn_input_mode = crate::input::FnInputMode::ImplBlock(&self_ty); 61 | let trait_dependency_mode = 62 | detect_trait_dependency_mode(&fn_input_mode, &trait_fns, &attr.crate_idents, trait_span)?; 63 | 64 | let impl_indirection = match attr.impl_kind { 65 | ImplKind::Static => generics::ImplIndirection::Static { ty: &self_ty }, 66 | ImplKind::DynRef => generics::ImplIndirection::Dynamic { ty: &self_ty }, 67 | }; 68 | 69 | let impl_block = fn_delegation_codegen::FnDelegationCodegen { 70 | opts: &attr.opts, 71 | crate_idents: &attr.crate_idents, 72 | trait_ref: &trait_path, 73 | trait_span, 74 | impl_indirection, 75 | trait_generics: &trait_generics, 76 | fn_input_mode: &fn_input_mode, 77 | trait_dependency_mode: &trait_dependency_mode, 78 | sub_attributes: &sub_attributes, 79 | } 80 | .gen_impl_block(&trait_fns); 81 | 82 | let inherent_sub_attrs = sub_attributes 83 | .iter() 84 | .filter(|sub_attr| !matches!(sub_attr, SubAttribute::AsyncTrait(_))); 85 | 86 | Ok(quote! { 87 | #(#inherent_sub_attrs)* 88 | #unsafety #impl_token #self_ty { 89 | #(#items)* 90 | } 91 | #impl_block 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_impl/input_attr.rs: -------------------------------------------------------------------------------- 1 | use crate::idents::CrateIdents; 2 | use crate::opt::*; 3 | 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | // Input of #[entrait(ref|dyn?)] impl A for B {} 7 | pub struct EntraitSimpleImplAttr { 8 | pub impl_kind: ImplKind, 9 | pub opts: Opts, 10 | pub crate_idents: CrateIdents, 11 | } 12 | 13 | #[derive(Clone, Copy)] 14 | pub enum ImplKind { 15 | Static, 16 | DynRef, 17 | } 18 | 19 | impl Parse for EntraitSimpleImplAttr { 20 | fn parse(input: ParseStream) -> syn::Result { 21 | let span = input.span(); 22 | 23 | let ref_token: Option = input.parse()?; 24 | let dyn_token: Option = input.parse()?; 25 | 26 | let mut debug = None; 27 | 28 | if !input.is_empty() { 29 | loop { 30 | match input.parse::()? { 31 | EntraitOpt::Debug(opt) => debug = Some(opt), 32 | entrait_opt => { 33 | return Err(syn::Error::new(entrait_opt.span(), "Unsupported option")) 34 | } 35 | }; 36 | 37 | if input.peek(syn::token::Comma) { 38 | input.parse::()?; 39 | } else { 40 | break; 41 | } 42 | } 43 | } 44 | 45 | Ok(Self { 46 | impl_kind: if dyn_token.is_some() || ref_token.is_some() { 47 | ImplKind::DynRef 48 | } else { 49 | ImplKind::Static 50 | }, 51 | opts: Opts { 52 | default_span: span, 53 | no_deps: None, 54 | debug, 55 | export: None, 56 | future_send: None, 57 | mock_api: None, 58 | unimock: None, 59 | mockall: None, 60 | }, 61 | crate_idents: CrateIdents::new(span), 62 | }) 63 | } 64 | } 65 | 66 | #[expect(unused)] 67 | pub struct EntraitImplAttr { 68 | pub opts: Opts, 69 | pub crate_idents: CrateIdents, 70 | } 71 | 72 | impl Parse for EntraitImplAttr { 73 | fn parse(input: ParseStream) -> syn::Result { 74 | let span = input.span(); 75 | 76 | let mut debug = None; 77 | 78 | if !input.is_empty() { 79 | loop { 80 | match input.parse::()? { 81 | EntraitOpt::Debug(opt) => debug = Some(opt), 82 | entrait_opt => { 83 | return Err(syn::Error::new(entrait_opt.span(), "Unsupported option")) 84 | } 85 | }; 86 | 87 | if input.peek(syn::token::Comma) { 88 | input.parse::()?; 89 | } else { 90 | break; 91 | } 92 | } 93 | } 94 | 95 | Ok(Self { 96 | opts: Opts { 97 | default_span: span, 98 | no_deps: None, 99 | debug, 100 | export: None, 101 | future_send: None, 102 | mock_api: None, 103 | unimock: None, 104 | mockall: None, 105 | }, 106 | crate_idents: CrateIdents::new(span), 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /entrait_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # entrait_macros 2 | //! 3 | //! Procedural macros used by entrait. 4 | 5 | #![forbid(unsafe_code)] 6 | 7 | extern crate proc_macro; 8 | 9 | use proc_macro::TokenStream; 10 | 11 | mod analyze_generics; 12 | mod attributes; 13 | mod entrait_fn; 14 | mod entrait_impl; 15 | mod entrait_trait; 16 | mod fn_delegation_codegen; 17 | mod generics; 18 | mod idents; 19 | mod input; 20 | mod opt; 21 | mod signature; 22 | mod sub_attributes; 23 | mod token_util; 24 | mod trait_codegen; 25 | 26 | use input::Input; 27 | use opt::Opts; 28 | 29 | #[proc_macro_attribute] 30 | pub fn entrait(attr: TokenStream, input: TokenStream) -> TokenStream { 31 | invoke(attr, input, |_| {}) 32 | } 33 | 34 | #[proc_macro_attribute] 35 | pub fn entrait_export(attr: TokenStream, input: TokenStream) -> TokenStream { 36 | invoke(attr, input, |opts| { 37 | set_fallbacks([&mut opts.export]); 38 | }) 39 | } 40 | 41 | #[proc_macro_attribute] 42 | pub fn entrait_unimock(attr: TokenStream, input: TokenStream) -> TokenStream { 43 | invoke(attr, input, |opts| { 44 | set_fallbacks([&mut opts.unimock]); 45 | }) 46 | } 47 | 48 | #[proc_macro_attribute] 49 | pub fn entrait_export_unimock(attr: TokenStream, input: TokenStream) -> TokenStream { 50 | invoke(attr, input, |opts| { 51 | set_fallbacks([&mut opts.export, &mut opts.unimock]); 52 | }) 53 | } 54 | 55 | fn set_fallbacks(opts: [&mut Option>; N]) { 56 | for opt in opts.into_iter() { 57 | opt.get_or_insert(opt::SpanOpt::of(true)); 58 | } 59 | } 60 | 61 | fn invoke( 62 | attr: proc_macro::TokenStream, 63 | input: proc_macro::TokenStream, 64 | opts_modifier: impl FnOnce(&mut Opts), 65 | ) -> proc_macro::TokenStream { 66 | let input = syn::parse_macro_input!(input as Input); 67 | 68 | let (result, debug) = match input { 69 | Input::Fn(input_fn) => { 70 | let mut attr = syn::parse_macro_input!(attr as entrait_fn::input_attr::EntraitFnAttr); 71 | opts_modifier(&mut attr.opts); 72 | 73 | ( 74 | entrait_fn::entrait_for_single_fn(&attr, input_fn), 75 | attr.opts.debug_value(), 76 | ) 77 | } 78 | Input::Mod(input_mod) => { 79 | let mut attr = syn::parse_macro_input!(attr as entrait_fn::input_attr::EntraitFnAttr); 80 | opts_modifier(&mut attr.opts); 81 | 82 | ( 83 | entrait_fn::entrait_for_mod(&attr, input_mod), 84 | attr.opts.debug_value(), 85 | ) 86 | } 87 | Input::Trait(item_trait) => { 88 | let mut attr = 89 | syn::parse_macro_input!(attr as entrait_trait::input_attr::EntraitTraitAttr); 90 | opts_modifier(&mut attr.opts); 91 | let debug = attr.opts.debug.map(|opt| *opt.value()).unwrap_or(false); 92 | 93 | (entrait_trait::output_tokens(attr, item_trait), debug) 94 | } 95 | Input::Impl(input_impl) => { 96 | let mut attr = 97 | syn::parse_macro_input!(attr as entrait_impl::input_attr::EntraitSimpleImplAttr); 98 | opts_modifier(&mut attr.opts); 99 | let debug = attr.opts.debug.map(|opt| *opt.value()).unwrap_or(false); 100 | 101 | ( 102 | entrait_impl::output_tokens_for_impl(attr, input_impl), 103 | debug, 104 | ) 105 | } 106 | }; 107 | 108 | let output = match result { 109 | Ok(token_stream) => token_stream, 110 | Err(err) => err.into_compile_error(), 111 | }; 112 | 113 | if debug { 114 | println!("{}", output); 115 | } 116 | 117 | proc_macro::TokenStream::from(output) 118 | } 119 | -------------------------------------------------------------------------------- /entrait_macros/src/signature/future.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use quote::ToTokens; 4 | 5 | use crate::generics::TraitIndirection; 6 | use crate::idents::CrateIdents; 7 | use crate::token_util::EmptyToken; 8 | use crate::token_util::Punctuator; 9 | 10 | use super::AssociatedFut; 11 | use super::EntraitSignature; 12 | 13 | pub struct FutDecl<'s> { 14 | pub signature: &'s EntraitSignature, 15 | pub associated_fut: &'s AssociatedFut, 16 | pub trait_indirection: TraitIndirection, 17 | pub crate_idents: &'s CrateIdents, 18 | } 19 | 20 | impl<'s> ToTokens for FutDecl<'s> { 21 | fn to_tokens(&self, stream: &mut TokenStream) { 22 | let ident = &self.associated_fut.ident; 23 | let core = &self.crate_idents.core; 24 | let output = &self.associated_fut.output; 25 | let base_lifetime = &self.associated_fut.base_lifetime; 26 | 27 | let params = FutParams { 28 | signature: self.signature, 29 | }; 30 | let where_clause = FutWhereClause { 31 | signature: self.signature, 32 | trait_indirection: self.trait_indirection, 33 | associated_fut: self.associated_fut, 34 | }; 35 | 36 | let tokens = quote! { 37 | #[allow(non_camel_case_types)] 38 | type #ident #params: ::#core::future::Future + Send + #base_lifetime #where_clause; 39 | }; 40 | 41 | tokens.to_tokens(stream); 42 | } 43 | } 44 | 45 | pub struct FutImpl<'s> { 46 | pub signature: &'s EntraitSignature, 47 | pub associated_fut: &'s AssociatedFut, 48 | pub trait_indirection: TraitIndirection, 49 | pub crate_idents: &'s CrateIdents, 50 | } 51 | 52 | impl<'s> ToTokens for FutImpl<'s> { 53 | fn to_tokens(&self, stream: &mut TokenStream) { 54 | let ident = &self.associated_fut.ident; 55 | let core = &self.crate_idents.core; 56 | let output = &self.associated_fut.output; 57 | 58 | let params = FutParams { 59 | signature: self.signature, 60 | }; 61 | let fut_bounds = FutImplBounds { 62 | associated_fut: self.associated_fut, 63 | }; 64 | let where_clause = FutWhereClause { 65 | signature: self.signature, 66 | trait_indirection: self.trait_indirection, 67 | associated_fut: self.associated_fut, 68 | }; 69 | 70 | let tokens = quote! { 71 | #[allow(non_camel_case_types)] 72 | type #ident #params = impl ::#core::future::Future #fut_bounds #where_clause; 73 | }; 74 | tokens.to_tokens(stream); 75 | } 76 | } 77 | 78 | struct FutWhereClause<'s> { 79 | signature: &'s EntraitSignature, 80 | trait_indirection: TraitIndirection, 81 | associated_fut: &'s AssociatedFut, 82 | } 83 | 84 | impl<'s> ToTokens for FutWhereClause<'s> { 85 | fn to_tokens(&self, stream: &mut TokenStream) { 86 | let base_lifetime = &self.associated_fut.base_lifetime; 87 | let mut punctuator = Punctuator::new( 88 | stream, 89 | quote! { where }, 90 | syn::token::Comma::default(), 91 | EmptyToken, 92 | ); 93 | 94 | for et_lifetime in self.signature.et_lifetimes_in_assoc_future_except_base() { 95 | let lt = &et_lifetime.lifetime; 96 | 97 | punctuator.push(quote! { 98 | #lt: #base_lifetime 99 | }); 100 | } 101 | 102 | punctuator.push_fn(|stream| { 103 | let bound_target = match self.trait_indirection { 104 | TraitIndirection::StaticImpl | TraitIndirection::DynamicImpl => quote! { EntraitT }, 105 | TraitIndirection::Plain | TraitIndirection::Trait => quote! { Self }, 106 | }; 107 | 108 | let outlives = self 109 | .signature 110 | .et_lifetimes_in_assoc_future() 111 | .map(|et| &et.lifetime); 112 | 113 | stream.extend(quote! { 114 | #bound_target: #(#outlives)+* 115 | }); 116 | }); 117 | } 118 | } 119 | 120 | struct FutImplBounds<'s> { 121 | associated_fut: &'s AssociatedFut, 122 | } 123 | 124 | impl<'s> ToTokens for FutImplBounds<'s> { 125 | fn to_tokens(&self, stream: &mut TokenStream) { 126 | let mut punctuator = Punctuator::new( 127 | stream, 128 | syn::token::Plus::default(), 129 | syn::token::Plus::default(), 130 | EmptyToken, 131 | ); 132 | 133 | punctuator.push(syn::Ident::new("Send", proc_macro2::Span::call_site())); 134 | punctuator.push(&self.associated_fut.base_lifetime); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/it/dependency_inversion.rs: -------------------------------------------------------------------------------- 1 | #[entrait::entrait(pub Baz)] 2 | fn baz(_: &D) -> i32 { 3 | 42 4 | } 5 | 6 | mod simple_static { 7 | use entrait::*; 8 | 9 | #[entrait(FoobarImpl, delegate_by = DelegateFoobar)] 10 | pub trait Foobar { 11 | fn foo(&self) -> i32; 12 | fn bar(&self) -> u32; 13 | } 14 | 15 | pub struct MyImpl2; 16 | 17 | #[entrait] 18 | impl FoobarImpl for MyImpl2 { 19 | fn bar(_: &D) -> u32 { 20 | 1337 21 | } 22 | 23 | fn foo(deps: &impl super::Baz) -> i32 { 24 | deps.baz() 25 | } 26 | } 27 | 28 | impl DelegateFoobar for () { 29 | type Target = MyImpl2; 30 | } 31 | 32 | #[test] 33 | fn test_mod() { 34 | let app = Impl::new(()); 35 | 36 | assert_eq!(42, app.foo()); 37 | } 38 | 39 | impl DelegateFoobar for bool { 40 | type Target = MyImpl2; 41 | } 42 | 43 | #[test] 44 | fn test_impl_block() { 45 | let app = Impl::new(true); 46 | 47 | assert_eq!(42, app.foo()); 48 | assert_eq!(1337, app.bar()); 49 | } 50 | } 51 | 52 | mod simple_dyn { 53 | use entrait::*; 54 | 55 | #[entrait(FoobarImpl, delegate_by=ref)] 56 | trait Foobar { 57 | fn foo(&self) -> i32; 58 | fn bar(&self) -> u32; 59 | } 60 | 61 | struct Implementor2; 62 | 63 | #[entrait(ref)] 64 | impl FoobarImpl for Implementor2 { 65 | pub fn bar(_: &D) -> u32 { 66 | 1337 67 | } 68 | 69 | pub fn foo(deps: &impl super::Baz) -> i32 { 70 | deps.baz() 71 | } 72 | } 73 | 74 | struct App { 75 | foobar: Box + Sync>, 76 | } 77 | 78 | impl AsRef> for App { 79 | fn as_ref(&self) -> &dyn FoobarImpl { 80 | self.foobar.as_ref() 81 | } 82 | } 83 | 84 | #[test] 85 | fn test_impl_block() { 86 | let app = Impl::new(App { 87 | foobar: Box::new(Implementor2), 88 | }); 89 | 90 | assert_eq!(42, app.foo()); 91 | assert_eq!(1337, app.bar()); 92 | } 93 | } 94 | 95 | mod async_static { 96 | use entrait::*; 97 | 98 | #[entrait(FoobarImpl, delegate_by = DelegateFoobar)] 99 | pub trait Foobar { 100 | async fn foo(&self) -> i32; 101 | async fn bar(&self) -> u32; 102 | } 103 | 104 | pub struct Implementor2; 105 | 106 | #[entrait] 107 | impl FoobarImpl for Implementor2 { 108 | pub async fn bar(_: &D) -> u32 { 109 | 1337 110 | } 111 | 112 | pub async fn foo(deps: &impl super::Baz) -> i32 { 113 | deps.baz() 114 | } 115 | } 116 | 117 | impl DelegateFoobar for bool { 118 | type Target = Implementor2; 119 | } 120 | 121 | #[tokio::test] 122 | async fn test_impl_block() { 123 | let app = Impl::new(true); 124 | 125 | assert_eq!(42, app.foo().await); 126 | assert_eq!(1337, app.bar().await); 127 | } 128 | } 129 | 130 | mod async_dyn { 131 | use entrait::*; 132 | 133 | #[entrait(FoobarImpl, delegate_by=ref)] 134 | #[async_trait::async_trait] 135 | pub trait Foobar { 136 | async fn foo(&self) -> i32; 137 | async fn bar(&self) -> u32; 138 | } 139 | 140 | pub struct Implementor2; 141 | 142 | #[entrait(ref)] 143 | #[async_trait::async_trait] 144 | impl FoobarImpl for Implementor2 { 145 | pub async fn bar(_: &D) -> u32 { 146 | 1337 147 | } 148 | 149 | pub async fn foo(deps: &impl super::Baz) -> i32 { 150 | deps.baz() 151 | } 152 | } 153 | 154 | struct App2(Implementor2); 155 | 156 | impl AsRef + Sync> for App2 { 157 | fn as_ref(&self) -> &(dyn FoobarImpl + Sync) { 158 | &self.0 159 | } 160 | } 161 | 162 | #[tokio::test] 163 | async fn test_impl_block() { 164 | let app = Impl::new(App2(Implementor2)); 165 | 166 | assert_eq!(42, app.foo().await); 167 | assert_eq!(1337, app.bar().await); 168 | } 169 | } 170 | 171 | mod issue_29 { 172 | use entrait::*; 173 | 174 | #[entrait(FoobarImpl, delegate_by = DelegateFoobar)] 175 | pub trait Foobar { 176 | fn foo<'a>(&self, input: &'a str) -> &'a str; 177 | } 178 | 179 | pub struct MyImpl; 180 | 181 | #[entrait] 182 | impl FoobarImpl for MyImpl { 183 | fn foo<'a, D>(deps: &D, input: &'a str) -> &'a str { 184 | input 185 | } 186 | } 187 | 188 | impl DelegateFoobar for () { 189 | type Target = MyImpl; 190 | } 191 | 192 | #[test] 193 | fn test_mod() { 194 | let app = Impl::new(()); 195 | assert_eq!("foo", app.foo("foo")); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_fn/mod.rs: -------------------------------------------------------------------------------- 1 | //! # entrait_macros 2 | //! 3 | //! Procedural macros used by entrait. 4 | //! 5 | 6 | pub mod input_attr; 7 | 8 | use crate::analyze_generics; 9 | use crate::analyze_generics::GenericsAnalyzer; 10 | use crate::analyze_generics::TraitFnAnalyzer; 11 | use crate::fn_delegation_codegen; 12 | use crate::generics; 13 | use crate::input::FnInputMode; 14 | use crate::input::{InputFn, InputMod, ModItem}; 15 | use crate::signature; 16 | use crate::sub_attributes::analyze_sub_attributes; 17 | use crate::trait_codegen::Supertraits; 18 | use crate::trait_codegen::TraitCodegen; 19 | use input_attr::*; 20 | 21 | use proc_macro2::TokenStream; 22 | use quote::quote; 23 | 24 | use crate::analyze_generics::detect_trait_dependency_mode; 25 | 26 | pub fn entrait_for_single_fn(attr: &EntraitFnAttr, input_fn: InputFn) -> syn::Result { 27 | let fn_input_mode = FnInputMode::SingleFn(&input_fn.fn_sig.ident); 28 | let mut generics_analyzer = GenericsAnalyzer::new(); 29 | 30 | let trait_fns = [TraitFnAnalyzer { 31 | impl_receiver_kind: signature::ImplReceiverKind::SelfRef, 32 | trait_span: attr.trait_ident.span(), 33 | crate_idents: &attr.crate_idents, 34 | opts: &attr.opts, 35 | } 36 | .analyze(input_fn.input_sig(), &mut generics_analyzer)?]; 37 | let sub_attributes = analyze_sub_attributes(&input_fn.fn_attrs); 38 | 39 | let trait_dependency_mode = detect_trait_dependency_mode( 40 | &fn_input_mode, 41 | &trait_fns, 42 | &attr.crate_idents, 43 | attr.trait_ident.span(), 44 | )?; 45 | let trait_generics = generics_analyzer.into_trait_generics(); 46 | let trait_def = TraitCodegen { 47 | opts: &attr.opts, 48 | crate_idents: &attr.crate_idents, 49 | trait_indirection: generics::TraitIndirection::Plain, 50 | trait_dependency_mode: &trait_dependency_mode, 51 | sub_attributes: &sub_attributes, 52 | } 53 | .gen_trait_def( 54 | &attr.trait_visibility, 55 | &attr.trait_ident, 56 | &trait_generics, 57 | &Supertraits::None, 58 | &trait_fns, 59 | &fn_input_mode, 60 | )?; 61 | 62 | let impl_block = fn_delegation_codegen::FnDelegationCodegen { 63 | opts: &attr.opts, 64 | crate_idents: &attr.crate_idents, 65 | trait_ref: &attr.trait_ident, 66 | trait_span: attr.trait_ident.span(), 67 | impl_indirection: generics::ImplIndirection::None, 68 | trait_generics: &trait_generics, 69 | fn_input_mode: &fn_input_mode, 70 | trait_dependency_mode: &trait_dependency_mode, 71 | sub_attributes: &sub_attributes, 72 | } 73 | .gen_impl_block(&trait_fns); 74 | 75 | let InputFn { 76 | fn_attrs, 77 | fn_vis, 78 | fn_sig, 79 | fn_body, 80 | .. 81 | } = input_fn; 82 | 83 | let out = quote! { 84 | #(#fn_attrs)* #fn_vis #fn_sig #fn_body 85 | #trait_def 86 | #impl_block 87 | }; 88 | 89 | // println!("\n\nfn output: {out}"); 90 | 91 | Ok(out) 92 | } 93 | 94 | pub fn entrait_for_mod(attr: &EntraitFnAttr, input_mod: InputMod) -> syn::Result { 95 | let fn_input_mode = FnInputMode::Module(&input_mod.ident); 96 | let mut generics_analyzer = analyze_generics::GenericsAnalyzer::new(); 97 | let trait_fns = input_mod 98 | .items 99 | .iter() 100 | .filter_map(ModItem::filter_pub_fn) 101 | .map(|input_fn| { 102 | TraitFnAnalyzer { 103 | impl_receiver_kind: signature::ImplReceiverKind::SelfRef, 104 | trait_span: attr.trait_ident.span(), 105 | crate_idents: &attr.crate_idents, 106 | opts: &attr.opts, 107 | } 108 | .analyze(input_fn.input_sig(), &mut generics_analyzer) 109 | }) 110 | .collect::>>()?; 111 | let sub_attributes = analyze_sub_attributes(&input_mod.attrs); 112 | 113 | let trait_dependency_mode = detect_trait_dependency_mode( 114 | &fn_input_mode, 115 | &trait_fns, 116 | &attr.crate_idents, 117 | attr.trait_ident.span(), 118 | )?; 119 | 120 | let trait_generics = generics_analyzer.into_trait_generics(); 121 | let trait_def = TraitCodegen { 122 | opts: &attr.opts, 123 | crate_idents: &attr.crate_idents, 124 | trait_indirection: generics::TraitIndirection::Plain, 125 | trait_dependency_mode: &trait_dependency_mode, 126 | sub_attributes: &sub_attributes, 127 | } 128 | .gen_trait_def( 129 | &attr.trait_visibility, 130 | &attr.trait_ident, 131 | &trait_generics, 132 | &Supertraits::None, 133 | &trait_fns, 134 | &fn_input_mode, 135 | )?; 136 | let impl_block = fn_delegation_codegen::FnDelegationCodegen { 137 | opts: &attr.opts, 138 | crate_idents: &attr.crate_idents, 139 | trait_ref: &attr.trait_ident, 140 | trait_span: attr.trait_ident.span(), 141 | impl_indirection: generics::ImplIndirection::None, 142 | trait_generics: &trait_generics, 143 | fn_input_mode: &fn_input_mode, 144 | trait_dependency_mode: &trait_dependency_mode, 145 | sub_attributes: &sub_attributes, 146 | } 147 | .gen_impl_block(&trait_fns); 148 | 149 | let InputMod { 150 | attrs, 151 | vis, 152 | mod_token, 153 | ident: mod_ident, 154 | items, 155 | .. 156 | } = input_mod; 157 | 158 | let trait_vis = &attr.trait_visibility; 159 | let trait_ident = &attr.trait_ident; 160 | 161 | Ok(quote! { 162 | #(#attrs)* 163 | #vis #mod_token #mod_ident { 164 | #(#items)* 165 | 166 | #trait_def 167 | #impl_block 168 | } 169 | 170 | #trait_vis use #mod_ident::#trait_ident; 171 | }) 172 | } 173 | -------------------------------------------------------------------------------- /entrait_macros/src/opt.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::parse::{Parse, ParseStream}; 3 | 4 | pub struct Opts { 5 | pub default_span: Span, 6 | 7 | pub no_deps: Option>, 8 | pub debug: Option>, 9 | 10 | /// Whether to export mocks (i.e. not gated with cfg(test)) 11 | pub export: Option>, 12 | 13 | pub future_send: Option>, 14 | 15 | pub mock_api: Option, 16 | 17 | /// Mocking with unimock 18 | pub unimock: Option>, 19 | 20 | /// Mocking with mockall 21 | pub mockall: Option>, 22 | } 23 | 24 | impl Opts { 25 | pub fn no_deps_value(&self) -> bool { 26 | self.default_option(self.no_deps, false).0 27 | } 28 | 29 | pub fn debug_value(&self) -> bool { 30 | self.default_option(self.debug, false).0 31 | } 32 | 33 | pub fn export_value(&self) -> bool { 34 | self.default_option(self.export, false).0 35 | } 36 | 37 | pub fn future_send(&self) -> FutureSend { 38 | self.default_option(self.future_send, FutureSend(true)).0 39 | } 40 | 41 | pub fn mockable(&self) -> Mockable { 42 | if (self.unimock.is_some() && self.mock_api.is_some()) || self.mockall.is_some() { 43 | Mockable::Yes 44 | } else { 45 | Mockable::No 46 | } 47 | } 48 | 49 | pub fn default_option(&self, option: Option>, default: T) -> SpanOpt { 50 | match option { 51 | Some(option) => option, 52 | None => SpanOpt(default, self.default_span), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, Copy)] 58 | pub enum Mockable { 59 | Yes, 60 | No, 61 | } 62 | 63 | impl Mockable { 64 | pub fn yes(self) -> bool { 65 | matches!(self, Self::Yes) 66 | } 67 | } 68 | 69 | #[derive(Clone)] 70 | #[allow(clippy::enum_variant_names)] 71 | pub enum Delegate { 72 | BySelf, 73 | ByRef(RefDelegate), 74 | ByTrait(syn::Ident), 75 | } 76 | 77 | #[derive(Clone)] 78 | pub enum RefDelegate { 79 | AsRef, 80 | Borrow, 81 | } 82 | 83 | #[derive(Clone, Copy)] 84 | pub struct FutureSend(pub bool); 85 | 86 | #[derive(Copy, Clone)] 87 | pub struct SpanOpt(pub T, pub Span); 88 | 89 | impl SpanOpt { 90 | pub fn of(value: T) -> Self { 91 | Self(value, proc_macro2::Span::call_site()) 92 | } 93 | 94 | pub fn value(&self) -> &T { 95 | &self.0 96 | } 97 | } 98 | 99 | /// 100 | /// "keyword args" to `entrait`. 101 | /// 102 | pub enum EntraitOpt { 103 | NoDeps(SpanOpt), 104 | Debug(SpanOpt), 105 | DelegateBy(SpanOpt), 106 | /// Whether to export mocks 107 | Export(SpanOpt), 108 | MaybeSend(SpanOpt), 109 | /// How to name the mock API 110 | MockApi(MockApiIdent), 111 | /// Whether to generate unimock impl 112 | Unimock(SpanOpt), 113 | /// Whether to generate mockall impl 114 | Mockall(SpanOpt), 115 | } 116 | 117 | impl EntraitOpt { 118 | pub fn span(&self) -> proc_macro2::Span { 119 | match self { 120 | Self::NoDeps(opt) => opt.1, 121 | Self::Debug(opt) => opt.1, 122 | Self::DelegateBy(opt) => opt.1, 123 | Self::MaybeSend(opt) => opt.1, 124 | Self::Export(opt) => opt.1, 125 | Self::MockApi(ident) => ident.0.span(), 126 | Self::Unimock(opt) => opt.1, 127 | Self::Mockall(opt) => opt.1, 128 | } 129 | } 130 | } 131 | 132 | impl Parse for EntraitOpt { 133 | fn parse(input: ParseStream) -> syn::Result { 134 | use EntraitOpt::*; 135 | 136 | if input.peek(syn::token::Question) { 137 | let _q: syn::token::Question = input.parse().unwrap(); 138 | 139 | let ident: syn::Ident = input.parse()?; 140 | let span = ident.span(); 141 | let ident_string = ident.to_string(); 142 | 143 | match ident_string.as_str() { 144 | "Send" => Ok(MaybeSend(SpanOpt(FutureSend(false), span))), 145 | _ => Err(syn::Error::new( 146 | span, 147 | format!("Unkonwn entrait option \"{ident_string}\""), 148 | )), 149 | } 150 | } else { 151 | let ident: syn::Ident = input.parse()?; 152 | let span = ident.span(); 153 | let ident_string = ident.to_string(); 154 | 155 | match ident_string.as_str() { 156 | "no_deps" => Ok(NoDeps(parse_eq_bool(input, true, span)?)), 157 | "debug" => Ok(Debug(parse_eq_bool(input, true, span)?)), 158 | "delegate_by" => Ok(DelegateBy(parse_eq_delegate_by( 159 | input, 160 | Delegate::BySelf, 161 | span, 162 | )?)), 163 | "export" => Ok(Export(parse_eq_bool(input, true, span)?)), 164 | "mock_api" => { 165 | let _: syn::token::Eq = input.parse()?; 166 | Ok(Self::MockApi(MockApiIdent(input.parse()?))) 167 | } 168 | "unimock" => Ok(Unimock(parse_eq_bool(input, true, span)?)), 169 | "mockall" => Ok(Mockall(parse_eq_bool(input, true, span)?)), 170 | _ => Err(syn::Error::new( 171 | span, 172 | format!("Unkonwn entrait option \"{ident_string}\""), 173 | )), 174 | } 175 | } 176 | } 177 | } 178 | 179 | pub struct MockApiIdent(pub syn::Ident); 180 | 181 | fn parse_eq_bool(input: ParseStream, default: bool, span: Span) -> syn::Result> { 182 | parse_eq_value_or_default(input, default, |b: syn::LitBool| Ok(b.value()), span) 183 | } 184 | 185 | fn parse_eq_delegate_by( 186 | input: ParseStream, 187 | default: Delegate, 188 | span: Span, 189 | ) -> syn::Result> { 190 | if !input.peek(syn::token::Eq) { 191 | return Ok(SpanOpt(default, span)); 192 | } 193 | 194 | input.parse::()?; 195 | 196 | if input.peek(syn::token::Ref) { 197 | let _: syn::token::Ref = input.parse()?; 198 | 199 | return Ok(SpanOpt(Delegate::ByRef(RefDelegate::AsRef), span)); 200 | } 201 | 202 | let ident = input.parse::()?; 203 | 204 | Ok(SpanOpt( 205 | match ident.to_string().as_str() { 206 | "Self" => Delegate::BySelf, 207 | "Borrow" => Delegate::ByRef(RefDelegate::Borrow), 208 | _ => Delegate::ByTrait(ident), 209 | }, 210 | span, 211 | )) 212 | } 213 | 214 | fn parse_eq_value_or_default( 215 | input: ParseStream, 216 | default_value: O, 217 | mapper: F, 218 | span: Span, 219 | ) -> syn::Result> 220 | where 221 | V: syn::parse::Parse, 222 | F: FnOnce(V) -> syn::Result, 223 | { 224 | if !input.peek(syn::token::Eq) { 225 | return Ok(SpanOpt(default_value, span)); 226 | } 227 | 228 | input.parse::()?; 229 | 230 | let parsed = input.parse::()?; 231 | 232 | Ok(SpanOpt(mapper(parsed)?, span)) 233 | } 234 | -------------------------------------------------------------------------------- /tests/it/simple.rs: -------------------------------------------------------------------------------- 1 | mod bounds { 2 | use entrait::*; 3 | 4 | mod app { 5 | pub struct State { 6 | pub number: u32, 7 | } 8 | } 9 | 10 | mod inline_bounds { 11 | use super::*; 12 | 13 | #[entrait(pub Foo)] 14 | fn foo(app: &A) -> u32 { 15 | println!("Foo"); 16 | app.bar(); 17 | app.baz("from foo") 18 | } 19 | } 20 | 21 | mod where_bounds { 22 | use super::*; 23 | 24 | #[entrait(pub Foo)] 25 | fn foo(app: &A) -> u32 26 | where 27 | A: Bar + Baz, 28 | { 29 | println!("Foo"); 30 | app.bar(); 31 | app.baz("from foo") 32 | } 33 | } 34 | 35 | mod impl_bounds { 36 | use super::*; 37 | 38 | #[entrait(pub Foo)] 39 | fn foo(deps: &(impl Bar + Baz)) -> u32 { 40 | println!("Foo"); 41 | deps.bar(); 42 | deps.baz("from foo") 43 | } 44 | } 45 | 46 | #[entrait(Bar)] 47 | fn bar(deps: &A) 48 | where 49 | A: Baz, 50 | { 51 | println!("Bar"); 52 | deps.baz("from bar"); 53 | } 54 | 55 | #[entrait(Baz)] 56 | fn baz(app: &app::State, from_where: &str) -> u32 { 57 | println!("Baz {from_where}"); 58 | app.number 59 | } 60 | 61 | #[test] 62 | fn test_where_bounds() { 63 | use where_bounds::Foo; 64 | let impl_state = Impl::new(app::State { number: 42 }); 65 | let result = impl_state.foo(); 66 | assert_eq!(42, result); 67 | } 68 | 69 | #[test] 70 | fn test_impl_bounds() { 71 | use impl_bounds::Foo; 72 | let impl_state = Impl::new(app::State { number: 42 }); 73 | let result = impl_state.foo(); 74 | assert_eq!(42, result); 75 | } 76 | 77 | mod mixed_inline_bounds { 78 | use super::*; 79 | 80 | #[entrait(pub Foo)] 81 | fn foo(deps: &D, arg: &E) {} 82 | } 83 | } 84 | 85 | mod no_deps_and_feign { 86 | use entrait::entrait; 87 | 88 | use feignhttp::get; 89 | 90 | #[entrait(NoDeps, no_deps)] 91 | fn no_deps(_a: i32, _b: i32) {} 92 | 93 | #[entrait(CallMyApi, no_deps)] 94 | #[get("https://my.api.org/api/{param}")] 95 | async fn call_my_api(#[path] param: String) -> feignhttp::Result {} 96 | } 97 | 98 | mod test_tracing_instrument { 99 | use entrait::entrait; 100 | use tracing::instrument; 101 | 102 | #[entrait(IWantToDebug1)] 103 | #[instrument(skip(deps))] 104 | fn i_want_to_debug1(deps: &impl OtherFunc) { 105 | deps.other_func(1337); 106 | } 107 | 108 | #[instrument(skip(deps))] 109 | #[entrait(IWantToDebug2)] 110 | fn i_want_to_debug2(deps: &impl OtherFunc) { 111 | deps.other_func(1337); 112 | } 113 | 114 | #[entrait(OtherFunc, no_deps)] 115 | fn other_func(_some_arg: i32) {} 116 | } 117 | 118 | mod test_entrait_for_trait { 119 | use entrait::*; 120 | 121 | #[entrait] 122 | trait Plain { 123 | fn method0(&self, arg: i32) -> i32; 124 | } 125 | 126 | #[entrait] 127 | trait Generic1 { 128 | fn generic_return(&self, arg: i32) -> T; 129 | fn generic_param(&self, arg: T) -> i32; 130 | } 131 | 132 | #[entrait] 133 | trait Generic2 { 134 | fn generic_return(&self, arg: i32) -> (T, U); 135 | fn generic_params(&self, arg0: T, arg1: U) -> i32; 136 | } 137 | 138 | impl Plain for () { 139 | fn method0(&self, arg: i32) -> i32 { 140 | 1337 141 | } 142 | } 143 | 144 | impl Plain for &'static str { 145 | fn method0(&self, arg: i32) -> i32 { 146 | 42 147 | } 148 | } 149 | 150 | #[test] 151 | fn entraited_trait_should_have_impl_impl() { 152 | assert_eq!(1337, Impl::new(()).method0(0)); 153 | assert_eq!(42, Impl::new("app").method0(0)); 154 | } 155 | } 156 | 157 | mod module { 158 | use entrait::*; 159 | use std::any::Any; 160 | 161 | #[entrait(pub Dep1)] 162 | fn dep1(_: &impl Any) {} 163 | 164 | #[entrait(pub Dep2)] 165 | fn dep2(_: &impl Any) {} 166 | 167 | #[entrait(pub EmptyModule)] 168 | mod empty_module {} 169 | 170 | #[entrait(pub FooBarBazQux)] 171 | mod foo_bar_baz_qux { 172 | use super::Dep1; 173 | 174 | pub fn foo(deps: &impl Dep1, arg: i32) {} 175 | 176 | struct Foo {} 177 | 178 | pub fn bar(deps: &impl super::Dep2, arg: &str) {} 179 | 180 | mod hei {} 181 | 182 | pub(super) fn baz(deps: &impl Dep1) {} 183 | 184 | const _: () = {}; 185 | 186 | pub(crate) fn qux(deps: &impl super::Dep2) { 187 | not_included(); 188 | } 189 | 190 | static S: &str = ""; 191 | 192 | fn not_included() {} 193 | } 194 | 195 | // There should be an automatic `pub use foo_bar_baz_qux::FooBarBazQux`; 196 | fn takes_foo_bar_baz_qux(deps: &impl FooBarBazQux) { 197 | deps.foo(42); 198 | deps.bar(""); 199 | deps.baz(); 200 | deps.qux(); 201 | } 202 | 203 | fn test() { 204 | let app = Impl::new(()); 205 | takes_foo_bar_baz_qux(&app); 206 | } 207 | 208 | #[entrait(PrivateTrait)] 209 | mod private_trait {} 210 | 211 | // This test is behind this flag because 212 | // we cannot have private/crate-private types in interfaces 213 | // implemented by external crates 214 | #[cfg(not(feature = "unimock"))] 215 | mod crate_private { 216 | use entrait::*; 217 | 218 | pub(crate) struct PrivateType; 219 | 220 | #[entrait(pub(crate) PubCrateTrait)] 221 | mod pub_crate_trait { 222 | pub(crate) fn foo(_: &D) -> super::PrivateType { 223 | super::PrivateType 224 | } 225 | } 226 | } 227 | 228 | // Note: pub(super) things will never work well, probably. 229 | // The macro cannot just append a another `::super`, because `pub(super::super)` is invalid syntax. 230 | } 231 | 232 | #[expect(unexpected_cfgs)] 233 | mod cfg_attributes { 234 | use entrait::*; 235 | 236 | #[entrait(mock_api = TraitMock)] 237 | trait Trait { 238 | fn compiled(&self); 239 | 240 | #[cfg(feature = "always-disabled")] 241 | fn not_compiled(&self) -> NonExistentType; 242 | } 243 | 244 | impl Trait for () { 245 | fn compiled(&self) {} 246 | } 247 | 248 | #[test] 249 | fn call_compiled() { 250 | let app = Impl::new(()); 251 | app.compiled(); 252 | } 253 | } 254 | 255 | mod future_send_opt_out { 256 | use std::rc::Rc; 257 | 258 | use entrait::*; 259 | 260 | #[entrait(Spawning, ?Send)] 261 | async fn spawning(deps: &(impl Bar + Clone + Send + Sync + 'static)) -> Rc { 262 | let deps = deps.clone(); 263 | 264 | tokio::task::spawn_local(async move { deps.bar().await }) 265 | .await 266 | .unwrap() 267 | } 268 | 269 | #[entrait(Bar, ?Send, mock_api = BarMock)] 270 | async fn bar(_: T) -> Rc { 271 | Rc::new(42) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /entrait_macros/src/trait_codegen.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, quote_spanned, ToTokens}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::{ 6 | analyze_generics::TraitFn, 7 | attributes, 8 | generics::{self, TraitDependencyMode, TraitIndirection}, 9 | idents::CrateIdents, 10 | input::FnInputMode, 11 | opt::{Opts, SpanOpt}, 12 | signature::EntraitSignature, 13 | sub_attributes::{contains_async_trait, SubAttribute}, 14 | token_util::push_tokens, 15 | }; 16 | 17 | pub struct TraitCodegen<'s> { 18 | pub opts: &'s Opts, 19 | pub crate_idents: &'s CrateIdents, 20 | pub trait_indirection: TraitIndirection, 21 | pub trait_dependency_mode: &'s TraitDependencyMode<'s, 's>, 22 | pub sub_attributes: &'s [SubAttribute<'s>], 23 | } 24 | 25 | impl TraitCodegen<'_> { 26 | pub fn gen_trait_def( 27 | &self, 28 | visibility: &syn::Visibility, 29 | trait_ident: &syn::Ident, 30 | trait_generics: &generics::TraitGenerics, 31 | supertraits: &Supertraits, 32 | trait_fns: &[TraitFn], 33 | fn_input_mode: &FnInputMode<'_>, 34 | ) -> syn::Result { 35 | let span = trait_ident.span(); 36 | 37 | let opt_unimock_attr = match self.opts.default_option(self.opts.unimock, false) { 38 | SpanOpt(true, span) => Some(attributes::ExportGatedAttr { 39 | params: attributes::UnimockAttrParams { 40 | trait_ident, 41 | mock_api: self.opts.mock_api.as_ref(), 42 | trait_indirection: self.trait_indirection, 43 | crate_idents: self.crate_idents, 44 | trait_fns, 45 | fn_input_mode, 46 | span, 47 | }, 48 | opts: self.opts, 49 | }), 50 | _ => None, 51 | }; 52 | 53 | let opt_entrait_for_trait_attr = match self.trait_dependency_mode { 54 | TraitDependencyMode::Concrete(_) => { 55 | Some(attributes::Attr(attributes::EntraitForTraitParams { 56 | crate_idents: self.crate_idents, 57 | })) 58 | } 59 | _ => None, 60 | }; 61 | 62 | let opt_mockall_automock_attr = match self.opts.default_option(self.opts.mockall, false) { 63 | SpanOpt(true, span) => Some(attributes::ExportGatedAttr { 64 | params: attributes::MockallAutomockParams { span }, 65 | opts: self.opts, 66 | }), 67 | _ => None, 68 | }; 69 | let trait_visibility = TraitVisibility { 70 | visibility, 71 | fn_input_mode, 72 | }; 73 | 74 | let fn_defs = trait_fns.iter().map(|trait_fn| { 75 | let attrs = &trait_fn.attrs; 76 | let trait_fn_sig = 77 | make_trait_fn_sig(&trait_fn.entrait_sig, self.sub_attributes, self.opts); 78 | 79 | quote! { 80 | #(#attrs)* 81 | #trait_fn_sig; 82 | } 83 | }); 84 | 85 | let params = trait_generics.trait_params(); 86 | let where_clause = trait_generics.trait_where_clause(); 87 | 88 | let trait_sub_attributes = self.sub_attributes.iter().filter(|attr| { 89 | matches!( 90 | attr, 91 | SubAttribute::AsyncTrait(_) | SubAttribute::Automock(_) 92 | ) 93 | }); 94 | 95 | Ok(quote_spanned! { span=> 96 | #opt_unimock_attr 97 | #opt_entrait_for_trait_attr 98 | #opt_mockall_automock_attr 99 | #(#trait_sub_attributes)* 100 | #trait_visibility trait #trait_ident #params #supertraits #where_clause { 101 | #(#fn_defs)* 102 | } 103 | }) 104 | } 105 | } 106 | 107 | #[derive(Clone)] 108 | pub enum Supertraits { 109 | None, 110 | Some { 111 | colon_token: syn::token::Colon, 112 | bounds: syn::punctuated::Punctuated, 113 | }, 114 | } 115 | 116 | impl ToTokens for Supertraits { 117 | fn to_tokens(&self, stream: &mut TokenStream) { 118 | if let Self::Some { 119 | colon_token, 120 | bounds, 121 | } = self 122 | { 123 | push_tokens!(stream, colon_token, bounds); 124 | } 125 | } 126 | } 127 | 128 | struct TraitVisibility<'a> { 129 | visibility: &'a syn::Visibility, 130 | fn_input_mode: &'a FnInputMode<'a>, 131 | } 132 | 133 | impl ToTokens for TraitVisibility<'_> { 134 | fn to_tokens(&self, stream: &mut TokenStream) { 135 | match &self.fn_input_mode { 136 | FnInputMode::Module(_) | FnInputMode::ImplBlock(_) => { 137 | match &self.visibility { 138 | syn::Visibility::Inherited => { 139 | // When the trait is "private", it should only be accessible to the module outside, 140 | // so use `pub(super)`. 141 | // This is because the trait is syntacitally "defined" outside the module, because 142 | // the attribute is an outer attribute. 143 | // If proc-macros supported inner attributes, and this was invoked with that, we wouldn't do this. 144 | push_tokens!(stream, syn::token::Pub(Span::call_site())); 145 | syn::token::Paren::default().surround(stream, |stream| { 146 | push_tokens!(stream, syn::token::Super::default()); 147 | }); 148 | } 149 | _ => { 150 | push_tokens!(stream, self.visibility); 151 | } 152 | } 153 | } 154 | FnInputMode::SingleFn(_) | FnInputMode::RawTrait(_) => { 155 | push_tokens!(stream, self.visibility); 156 | } 157 | } 158 | } 159 | } 160 | 161 | fn make_trait_fn_sig( 162 | entrait_sig: &EntraitSignature, 163 | sub_attributes: &[SubAttribute], 164 | opts: &Opts, 165 | ) -> syn::Signature { 166 | let mut sig = entrait_sig.sig.clone(); 167 | 168 | if entrait_sig.sig.asyncness.is_some() && !contains_async_trait(sub_attributes) { 169 | sig.asyncness = None; 170 | 171 | let mut return_type = syn::ReturnType::Default; 172 | std::mem::swap(&mut return_type, &mut sig.output); 173 | 174 | let span = return_type.span(); 175 | 176 | let output_type: syn::Type = match return_type { 177 | syn::ReturnType::Default => syn::parse_quote! { () }, 178 | syn::ReturnType::Type(_, ty) => *ty, 179 | }; 180 | 181 | let mut bounds: Vec = vec![quote! { 182 | ::core::future::Future 183 | }]; 184 | 185 | if opts.future_send().0 { 186 | bounds.push(quote! { 187 | ::core::marker::Send 188 | }); 189 | } 190 | 191 | sig.output = syn::parse_quote_spanned! {span=> 192 | -> impl #(#bounds)+* 193 | }; 194 | } 195 | 196 | sig 197 | } 198 | -------------------------------------------------------------------------------- /entrait_macros/src/fn_delegation_codegen.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro2::TokenStream; 3 | use quote::ToTokens; 4 | use quote::{quote, quote_spanned}; 5 | use syn::spanned::Spanned; 6 | 7 | use crate::analyze_generics::TraitFn; 8 | use crate::generics; 9 | use crate::generics::ImplIndirection; 10 | use crate::generics::TraitDependencyMode; 11 | use crate::idents::CrateIdents; 12 | use crate::input::FnInputMode; 13 | use crate::opt::Mockable; 14 | use crate::opt::Opts; 15 | use crate::sub_attributes::SubAttribute; 16 | use crate::token_util::push_tokens; 17 | use crate::token_util::TokenPair; 18 | 19 | /// Generate impls that call standalone generic functions 20 | pub struct FnDelegationCodegen<'s, TR> { 21 | pub opts: &'s Opts, 22 | #[expect(unused)] 23 | pub crate_idents: &'s CrateIdents, 24 | pub trait_ref: &'s TR, 25 | pub trait_span: Span, 26 | pub impl_indirection: ImplIndirection<'s>, 27 | pub trait_generics: &'s generics::TraitGenerics, 28 | pub fn_input_mode: &'s FnInputMode<'s>, 29 | pub trait_dependency_mode: &'s TraitDependencyMode<'s, 's>, 30 | pub sub_attributes: &'s [SubAttribute<'s>], 31 | } 32 | 33 | impl FnDelegationCodegen<'_, TR> { 34 | /// 35 | /// Generate code like 36 | /// 37 | /// ```no_compile 38 | /// impl<__T: ::entrait::Impl + Deps> Trait for __T { 39 | /// fn the_func(&self, args...) { 40 | /// the_func(self, args) 41 | /// } 42 | /// } 43 | /// ``` 44 | /// 45 | pub fn gen_impl_block(&self, trait_fns: &[TraitFn]) -> TokenStream { 46 | let params = self.trait_generics.impl_params( 47 | self.trait_dependency_mode, 48 | generics::has_any_self_by_value(trait_fns.iter().map(|trait_fn| trait_fn.sig())), 49 | ); 50 | let args = self.trait_generics.arguments(&self.impl_indirection); 51 | let self_ty = SelfTy { 52 | trait_dependency_mode: self.trait_dependency_mode, 53 | impl_indirection: &self.impl_indirection, 54 | mockable: self.opts.mockable(), 55 | span: self.trait_span, 56 | }; 57 | let where_clause = self.trait_generics.impl_where_clause( 58 | trait_fns, 59 | self.trait_dependency_mode, 60 | &self.impl_indirection, 61 | self.trait_span, 62 | ); 63 | 64 | let opt_self_scoping = if let FnInputMode::ImplBlock(ty) = self.fn_input_mode { 65 | Some(TokenPair( 66 | syn::token::SelfType(ty.span()), 67 | syn::token::PathSep(ty.span()), 68 | )) 69 | } else { 70 | None 71 | }; 72 | 73 | let items = trait_fns.iter().map(|trait_fn| { 74 | let fn_item = self.gen_delegating_fn_item(trait_fn, self.trait_span, &opt_self_scoping); 75 | 76 | quote! { 77 | #fn_item 78 | } 79 | }); 80 | 81 | let trait_impl_sub_attributes = self 82 | .sub_attributes 83 | .iter() 84 | .copied() 85 | .filter(|sub_attr| matches!(sub_attr, SubAttribute::AsyncTrait(_))); 86 | 87 | let trait_span = self.trait_span; 88 | let trait_ref = &self.trait_ref; 89 | 90 | quote_spanned! { trait_span=> 91 | #(#trait_impl_sub_attributes)* 92 | impl #params #trait_ref #args for #self_ty #where_clause { 93 | #(#items)* 94 | } 95 | } 96 | } 97 | 98 | /// Generate the fn (in the impl block) that calls the entraited fn 99 | fn gen_delegating_fn_item( 100 | &self, 101 | trait_fn: &TraitFn, 102 | span: Span, 103 | opt_self_scoping: &impl ToTokens, 104 | ) -> TokenStream { 105 | let entrait_sig = &trait_fn.entrait_sig; 106 | let trait_fn_sig = &trait_fn.sig(); 107 | let deps = &trait_fn.deps; 108 | 109 | let mut fn_ident = trait_fn.sig().ident.clone(); 110 | fn_ident.set_span(span); 111 | 112 | let opt_self_comma = match (deps, entrait_sig.sig.inputs.first(), &self.impl_indirection) { 113 | (generics::FnDeps::NoDeps { .. }, _, _) | (_, None, _) => None, 114 | (_, _, ImplIndirection::Static { .. } | ImplIndirection::Dynamic { .. }) => None, 115 | (_, Some(_), _) => Some(SelfArgComma(&self.impl_indirection, span)), 116 | }; 117 | 118 | let arguments = entrait_sig 119 | .sig 120 | .inputs 121 | .iter() 122 | .filter_map(|fn_arg| match fn_arg { 123 | syn::FnArg::Receiver(_) => None, 124 | syn::FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { 125 | syn::Pat::Ident(pat_ident) => Some(&pat_ident.ident), 126 | _ => { 127 | panic!("Found a non-ident pattern, this should be handled in signature.rs") 128 | } 129 | }, 130 | }); 131 | 132 | let opt_dot_await = trait_fn.opt_dot_await(span); 133 | 134 | quote_spanned! { span=> 135 | #trait_fn_sig { 136 | #opt_self_scoping #fn_ident(#opt_self_comma #(#arguments),*) #opt_dot_await 137 | } 138 | } 139 | } 140 | } 141 | 142 | struct SelfTy<'g, 'c> { 143 | trait_dependency_mode: &'g TraitDependencyMode<'g, 'c>, 144 | impl_indirection: &'g ImplIndirection<'g>, 145 | mockable: Mockable, 146 | span: Span, 147 | } 148 | 149 | impl quote::ToTokens for SelfTy<'_, '_> { 150 | fn to_tokens(&self, stream: &mut TokenStream) { 151 | match &self.trait_dependency_mode { 152 | TraitDependencyMode::Generic(idents) => match self.impl_indirection { 153 | ImplIndirection::None => { 154 | if self.mockable.yes() { 155 | push_tokens!(stream, idents.impl_path(self.span)) 156 | } else { 157 | push_tokens!(stream, idents.impl_t) 158 | } 159 | } 160 | ImplIndirection::Static { ty } => { 161 | push_tokens!(stream, ty); 162 | } 163 | ImplIndirection::Dynamic { ty } => { 164 | push_tokens!(stream, ty); 165 | } 166 | }, 167 | TraitDependencyMode::Concrete(ty) => { 168 | push_tokens!(stream, ty) 169 | } 170 | } 171 | } 172 | } 173 | 174 | // i.e. `self,` 175 | struct SelfArgComma<'g>(&'g ImplIndirection<'g>, Span); 176 | 177 | impl quote::ToTokens for SelfArgComma<'_> { 178 | fn to_tokens(&self, stream: &mut TokenStream) { 179 | let span = self.1; 180 | match &self.0 { 181 | ImplIndirection::None => { 182 | push_tokens!(stream, syn::token::SelfValue(span), syn::token::Comma(span)); 183 | } 184 | ImplIndirection::Static { .. } => { 185 | push_tokens!( 186 | stream, 187 | syn::Ident::new("__impl", span), 188 | syn::token::Comma(span) 189 | ); 190 | } 191 | ImplIndirection::Dynamic { .. } => { 192 | push_tokens!( 193 | stream, 194 | syn::Ident::new("__impl", span), 195 | syn::token::Comma(span) 196 | ); 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /entrait_macros/src/signature/fn_params.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use syn::visit_mut::VisitMut; 3 | 4 | #[derive(Copy, Clone, Eq, PartialEq)] 5 | enum ParamStatus { 6 | Ok, 7 | NeedsFix, 8 | } 9 | 10 | impl ParamStatus { 11 | fn is_ok(self) -> bool { 12 | matches!(self, Self::Ok) 13 | } 14 | 15 | fn combine(self, other: ParamStatus) -> Self { 16 | match (self, other) { 17 | (Self::Ok, Self::Ok) => Self::Ok, 18 | _ => Self::NeedsFix, 19 | } 20 | } 21 | } 22 | 23 | pub fn fix_fn_param_idents(sig: &mut syn::Signature) { 24 | if fix_ident_conflicts(sig).is_ok() { 25 | return; 26 | } 27 | 28 | if lift_inner_pat_idents(sig).is_ok() { 29 | return; 30 | } 31 | 32 | autogenerate_for_non_idents(sig); 33 | } 34 | 35 | fn fix_ident_conflicts(sig: &mut syn::Signature) -> ParamStatus { 36 | let mut status = ParamStatus::Ok; 37 | let fn_ident_string = sig.ident.to_string(); 38 | 39 | for fn_arg in sig.inputs.iter_mut() { 40 | let arg_status = match fn_arg { 41 | syn::FnArg::Receiver(_) => ParamStatus::Ok, 42 | syn::FnArg::Typed(pat_type) => match pat_type.pat.as_mut() { 43 | syn::Pat::Ident(param_ident) => { 44 | if param_ident.ident == fn_ident_string { 45 | param_ident.ident = syn::Ident::new( 46 | &format!("{}_", param_ident.ident), 47 | param_ident.ident.span(), 48 | ); 49 | } 50 | 51 | ParamStatus::Ok 52 | } 53 | _ => ParamStatus::NeedsFix, 54 | }, 55 | }; 56 | 57 | status = status.combine(arg_status); 58 | } 59 | 60 | status 61 | } 62 | 63 | fn lift_inner_pat_idents(sig: &mut syn::Signature) -> ParamStatus { 64 | fn try_lift_unambiguous_inner(pat: &mut syn::Pat) -> ParamStatus { 65 | struct PatIdentSearcher { 66 | first_binding_pat_ident: Option, 67 | binding_pat_count: usize, 68 | } 69 | 70 | impl syn::visit_mut::VisitMut for PatIdentSearcher { 71 | fn visit_pat_ident_mut(&mut self, i: &mut syn::PatIdent) { 72 | let ident_string = i.ident.to_string(); 73 | 74 | match ident_string.chars().next() { 75 | Some(char) if char.is_lowercase() => { 76 | self.binding_pat_count += 1; 77 | if self.first_binding_pat_ident.is_none() { 78 | self.first_binding_pat_ident = Some(i.ident.clone()); 79 | } 80 | } 81 | _ => {} 82 | } 83 | } 84 | } 85 | 86 | let mut searcher = PatIdentSearcher { 87 | first_binding_pat_ident: None, 88 | binding_pat_count: 0, 89 | }; 90 | 91 | searcher.visit_pat_mut(pat); 92 | 93 | if searcher.binding_pat_count == 1 { 94 | let ident = searcher.first_binding_pat_ident; 95 | *pat = syn::parse_quote! { #ident }; 96 | 97 | ParamStatus::Ok 98 | } else { 99 | ParamStatus::NeedsFix 100 | } 101 | } 102 | 103 | let mut status = ParamStatus::Ok; 104 | 105 | for fn_arg in &mut sig.inputs { 106 | let param_status = match fn_arg { 107 | syn::FnArg::Receiver(_) => ParamStatus::Ok, 108 | syn::FnArg::Typed(pat_type) => match pat_type.pat.as_mut() { 109 | syn::Pat::Ident(_) => ParamStatus::Ok, 110 | pat => try_lift_unambiguous_inner(pat), 111 | }, 112 | }; 113 | 114 | status = status.combine(param_status); 115 | } 116 | 117 | status 118 | } 119 | 120 | fn autogenerate_for_non_idents(sig: &mut syn::Signature) { 121 | let mut taken_idents: HashSet = sig 122 | .inputs 123 | .iter() 124 | .filter_map(|fn_arg| match fn_arg { 125 | syn::FnArg::Receiver(_) => None, 126 | syn::FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { 127 | syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()), 128 | _ => None, 129 | }, 130 | }) 131 | .collect(); 132 | 133 | fn generate_ident(index: usize, attempts: usize, taken_idents: &mut HashSet) -> String { 134 | let ident = format!( 135 | "{}arg{}", 136 | (0..attempts).map(|_| '_').collect::(), 137 | index, 138 | ); 139 | 140 | if taken_idents.contains(&ident) { 141 | generate_ident(index, attempts + 1, taken_idents) 142 | } else { 143 | taken_idents.insert(ident.clone()); 144 | ident 145 | } 146 | } 147 | 148 | let pat_type_args = sig.inputs.iter_mut().filter_map(|fn_arg| match fn_arg { 149 | syn::FnArg::Typed(pat_type) => Some(pat_type), 150 | _ => None, 151 | }); 152 | 153 | for (index, pat_type_arg) in pat_type_args.enumerate() { 154 | match pat_type_arg.pat.as_mut() { 155 | syn::Pat::Ident(_) => {} 156 | _ => { 157 | let new_ident_string = generate_ident(index, 0, &mut taken_idents); 158 | let new_ident = quote::format_ident!("{}", new_ident_string); 159 | *pat_type_arg.pat = syn::parse_quote! { #new_ident }; 160 | } 161 | } 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use quote::ToTokens; 168 | 169 | use super::*; 170 | 171 | fn convert_expect(mut source: syn::Signature, expected: syn::Signature) { 172 | fix_fn_param_idents(&mut source); 173 | assert_eq!( 174 | source.to_token_stream().to_string(), 175 | expected.to_token_stream().to_string(), 176 | ); 177 | } 178 | 179 | #[test] 180 | fn should_not_generate_conflicts() { 181 | convert_expect( 182 | syn::parse_quote! { 183 | fn foo(arg1: T, _: T, T(arg3): T, _: T) 184 | }, 185 | syn::parse_quote! { 186 | fn foo(arg1: T, _arg1: T, arg3: T, _arg3: T) 187 | }, 188 | ); 189 | 190 | convert_expect( 191 | syn::parse_quote! { 192 | fn foo(_: T, T(arg0): T) 193 | }, 194 | syn::parse_quote! { 195 | fn foo(_arg0: T, arg0: T) 196 | }, 197 | ); 198 | } 199 | 200 | #[test] 201 | fn should_extract_only_unambiguous_pat_idents() { 202 | convert_expect( 203 | syn::parse_quote! { 204 | fn f( 205 | ident0: T, 206 | T(ident1): T, 207 | T(T(ident2)): T, 208 | T(ident3, _): T, 209 | T(ident4, None): T, 210 | T(None): T, 211 | T(foo, bar): T, 212 | T(foo, T(bar)): T, 213 | ) 214 | }, 215 | syn::parse_quote! { 216 | fn f( 217 | ident0: T, 218 | ident1: T, 219 | ident2: T, 220 | ident3: T, 221 | ident4: T, 222 | arg5: T, 223 | arg6: T, 224 | arg7: T, 225 | ) 226 | }, 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /entrait_macros/src/signature/converter.rs: -------------------------------------------------------------------------------- 1 | use super::{fn_params, ReceiverGeneration}; 2 | use super::{EntraitSignature, ImplReceiverKind, InputSig}; 3 | use crate::{generics::FnDeps, idents::CrateIdents, opt::Opts}; 4 | 5 | use proc_macro2::Span; 6 | use syn::spanned::Spanned; 7 | 8 | pub struct SignatureConverter<'a> { 9 | pub crate_idents: &'a CrateIdents, 10 | #[expect(unused)] 11 | pub trait_span: Span, 12 | #[expect(unused)] 13 | pub opts: &'a Opts, 14 | pub input_sig: InputSig<'a>, 15 | pub deps: &'a FnDeps, 16 | pub impl_receiver_kind: ImplReceiverKind, 17 | } 18 | 19 | impl SignatureConverter<'_> { 20 | /// Convert from an standalone `fn` signature to a trait `fn` signature. 21 | pub fn convert_fn_to_trait_fn(&self) -> EntraitSignature { 22 | let mut entrait_sig = EntraitSignature::new(self.input_sig.sig.clone()); 23 | 24 | // strip away attributes 25 | for fn_arg in entrait_sig.sig.inputs.iter_mut() { 26 | match fn_arg { 27 | syn::FnArg::Receiver(receiver) => { 28 | receiver.attrs = vec![]; 29 | } 30 | syn::FnArg::Typed(pat_type) => { 31 | pat_type.attrs = vec![]; 32 | } 33 | } 34 | } 35 | 36 | let receiver_generation = self.detect_receiver_generation(&entrait_sig.sig); 37 | self.generate_params(&mut entrait_sig.sig, receiver_generation); 38 | 39 | self.remove_generic_type_params(&mut entrait_sig.sig); 40 | tidy_generics(&mut entrait_sig.sig.generics); 41 | 42 | fn_params::fix_fn_param_idents(&mut entrait_sig.sig); 43 | 44 | entrait_sig 45 | } 46 | 47 | fn detect_receiver_generation(&self, sig: &syn::Signature) -> ReceiverGeneration { 48 | match self.deps { 49 | FnDeps::NoDeps { .. } => ReceiverGeneration::Insert, 50 | _ => { 51 | if sig.inputs.is_empty() { 52 | ReceiverGeneration::None // bug? 53 | } else { 54 | ReceiverGeneration::Rewrite 55 | } 56 | } 57 | } 58 | } 59 | 60 | fn generate_params(&self, sig: &mut syn::Signature, receiver_generation: ReceiverGeneration) { 61 | match receiver_generation { 62 | ReceiverGeneration::Insert => { 63 | sig.inputs.insert( 64 | 0, 65 | self.gen_first_receiver( 66 | Span::call_site(), 67 | Some((syn::token::And::default(), None)), 68 | ), 69 | ); 70 | } 71 | ReceiverGeneration::Rewrite => { 72 | let input = sig.inputs.first_mut().unwrap(); 73 | let input_span = input.span(); 74 | match input { 75 | syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() { 76 | syn::Type::Reference(type_reference) => { 77 | let and_token = type_reference.and_token; 78 | let lifetime = type_reference.lifetime.clone(); 79 | 80 | *input = self 81 | .gen_first_receiver(pat_type.span(), Some((and_token, lifetime))); 82 | } 83 | _ => { 84 | let first_mut = sig.inputs.first_mut().unwrap(); 85 | *first_mut = self.gen_first_receiver(input_span, None); 86 | } 87 | }, 88 | syn::FnArg::Receiver(_) => panic!(), 89 | } 90 | } 91 | ReceiverGeneration::None => {} 92 | } 93 | 94 | if matches!(self.impl_receiver_kind, ImplReceiverKind::DynamicImpl) { 95 | sig.inputs 96 | .insert(1, self.gen_impl_receiver(Span::call_site())); 97 | } 98 | } 99 | 100 | fn gen_first_receiver( 101 | &self, 102 | span: Span, 103 | reference: Option<(syn::token::And, Option)>, 104 | ) -> syn::FnArg { 105 | match &self.impl_receiver_kind { 106 | ImplReceiverKind::SelfRef | ImplReceiverKind::DynamicImpl => { 107 | self.gen_self_receiver(span, reference) 108 | } 109 | ImplReceiverKind::StaticImpl => self.gen_impl_receiver(span), 110 | } 111 | } 112 | 113 | fn gen_self_receiver( 114 | &self, 115 | span: Span, 116 | reference: Option<(syn::token::And, Option)>, 117 | ) -> syn::FnArg { 118 | let ty = match reference { 119 | Some(_) => syn::parse_quote!(&Self), 120 | None => syn::parse_quote!(Self), 121 | }; 122 | 123 | syn::FnArg::Receiver(syn::Receiver { 124 | attrs: vec![], 125 | reference, 126 | mutability: None, 127 | self_token: syn::token::SelfValue(span), 128 | colon_token: None, 129 | ty, 130 | }) 131 | } 132 | 133 | fn gen_impl_receiver(&self, _: Span) -> syn::FnArg { 134 | let entrait = &self.crate_idents.entrait; 135 | syn::parse_quote! { 136 | __impl: &::#entrait::Impl 137 | } 138 | } 139 | 140 | fn remove_generic_type_params(&self, sig: &mut syn::Signature) { 141 | let deps_ident = match &self.deps { 142 | FnDeps::Generic { generic_param, .. } => generic_param.as_ref(), 143 | _ => None, 144 | }; 145 | 146 | let generics = &mut sig.generics; 147 | let mut params = syn::punctuated::Punctuated::new(); 148 | std::mem::swap(&mut params, &mut generics.params); 149 | 150 | for param in params.into_iter() { 151 | match ¶m { 152 | syn::GenericParam::Type(_) => {} 153 | _ => { 154 | generics.params.push(param); 155 | } 156 | } 157 | } 158 | 159 | if let Some(where_clause) = &mut generics.where_clause { 160 | let mut predicates = syn::punctuated::Punctuated::new(); 161 | std::mem::swap(&mut predicates, &mut where_clause.predicates); 162 | 163 | for predicate in predicates.into_iter() { 164 | match &predicate { 165 | syn::WherePredicate::Type(pred) => { 166 | if let Some(deps_ident) = &deps_ident { 167 | if !is_type_eq_ident(&pred.bounded_ty, deps_ident) { 168 | where_clause.predicates.push(predicate); 169 | } 170 | } else { 171 | where_clause.predicates.push(predicate); 172 | } 173 | } 174 | _ => { 175 | where_clause.predicates.push(predicate); 176 | } 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | fn is_type_eq_ident(ty: &syn::Type, ident: &syn::Ident) -> bool { 184 | match ty { 185 | syn::Type::Path(type_path) if type_path.path.segments.len() == 1 => { 186 | type_path.path.segments.first().unwrap().ident == *ident 187 | } 188 | _ => false, 189 | } 190 | } 191 | 192 | fn tidy_generics(generics: &mut syn::Generics) { 193 | if generics 194 | .where_clause 195 | .as_ref() 196 | .map(|cl| cl.predicates.is_empty()) 197 | .unwrap_or(false) 198 | { 199 | generics.where_clause = None; 200 | } 201 | 202 | if generics.params.is_empty() { 203 | generics.lt_token = None; 204 | generics.gt_token = None; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /entrait_macros/src/signature/lifetimes.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | EntraitLifetime, ReceiverGeneration, SigComponent, UsedInOutput, UserProvidedLifetime, 3 | }; 4 | 5 | use std::collections::HashSet; 6 | use syn::visit_mut::VisitMut; 7 | 8 | /// Looks at elided lifetimes and makes them explicit. 9 | /// Also collects all lifetimes into `et_lifetimes`. 10 | struct LifetimeMutVisitor { 11 | current_component: SigComponent, 12 | elided_params: HashSet, 13 | registered_user_lifetimes: HashSet, 14 | et_lifetimes: Vec, 15 | } 16 | 17 | impl LifetimeMutVisitor { 18 | fn new(elided_params: HashSet) -> Self { 19 | Self { 20 | current_component: SigComponent::Receiver, 21 | elided_params, 22 | registered_user_lifetimes: HashSet::new(), 23 | et_lifetimes: vec![], 24 | } 25 | } 26 | 27 | fn de_elide_receiver(&mut self, arg: &mut syn::FnArg) { 28 | self.current_component = SigComponent::Receiver; 29 | self.visit_fn_arg_mut(arg); 30 | } 31 | 32 | fn de_elide_param(&mut self, index: usize, arg: &mut syn::FnArg) { 33 | self.current_component = SigComponent::Param(index); 34 | self.visit_fn_arg_mut(arg); 35 | } 36 | 37 | fn de_elide_output(&mut self, output: &mut syn::ReturnType) { 38 | self.current_component = SigComponent::Output; 39 | self.visit_return_type_mut(output); 40 | } 41 | 42 | fn make_lifetime_explicit(&mut self, lifetime: Option) -> syn::Lifetime { 43 | match self.current_component { 44 | SigComponent::Receiver | SigComponent::Param(_) => match lifetime { 45 | Some(lifetime) => self.register_user_lifetime(lifetime), 46 | None => self.register_new_entrait_lifetime(), 47 | }, 48 | // Do not register user-provided output lifetimes, should already be registered from inputs: 49 | SigComponent::Output => { 50 | if let Some(lifetime) = &lifetime { 51 | self.tag_used_in_output(lifetime); 52 | } 53 | 54 | lifetime 55 | // If lifetime was elided, try to find it: 56 | .or_else(|| self.locate_output_lifetime()) 57 | // If not, there must be some kind of compile error somewhere else 58 | .unwrap_or_else(|| self.broken_lifetime()) 59 | } 60 | SigComponent::Base => panic!("The base lifetime is always explicit"), 61 | } 62 | } 63 | 64 | fn tag_used_in_output(&mut self, lifetime: &syn::Lifetime) { 65 | let mut et_lifetime = self 66 | .et_lifetimes 67 | .iter_mut() 68 | .find(|et| et.lifetime == *lifetime); 69 | 70 | if let Some(et_lifetime) = et_lifetime.as_mut() { 71 | et_lifetime.used_in_output.0 = true; 72 | } 73 | } 74 | 75 | fn locate_output_lifetime(&mut self) -> Option { 76 | let from_component = match self.only_elided_input() { 77 | // If only one input was elided, use that input: 78 | Some(elided_input) => SigComponent::Param(elided_input), 79 | // If not, use the receiver lifetime: 80 | None => SigComponent::Receiver, 81 | }; 82 | 83 | let mut et_lifetime = self 84 | .et_lifetimes 85 | .iter_mut() 86 | .find(|lt| lt.source == from_component); 87 | 88 | if let Some(et_lifetime) = et_lifetime.as_mut() { 89 | et_lifetime.used_in_output.0 = true; 90 | } 91 | 92 | et_lifetime.map(|et| et.lifetime.clone()) 93 | } 94 | 95 | fn only_elided_input(&self) -> Option { 96 | if self.elided_params.len() == 1 { 97 | self.elided_params.iter().next().copied() 98 | } else { 99 | None 100 | } 101 | } 102 | 103 | fn register_user_lifetime(&mut self, lifetime: syn::Lifetime) -> syn::Lifetime { 104 | let lifetime_string = lifetime.to_string(); 105 | if self.registered_user_lifetimes.contains(&lifetime_string) { 106 | lifetime 107 | } else { 108 | self.registered_user_lifetimes.insert(lifetime_string); 109 | self.register_lifetime(EntraitLifetime { 110 | lifetime, 111 | source: self.current_component, 112 | user_provided: UserProvidedLifetime(true), 113 | used_in_output: UsedInOutput(false), 114 | }) 115 | } 116 | } 117 | 118 | fn register_new_entrait_lifetime(&mut self) -> syn::Lifetime { 119 | let index = self.et_lifetimes.len(); 120 | self.register_lifetime(EntraitLifetime { 121 | lifetime: syn::Lifetime::new( 122 | &format!("'entrait{}", index), 123 | proc_macro2::Span::call_site(), 124 | ), 125 | source: self.current_component, 126 | user_provided: UserProvidedLifetime(false), 127 | used_in_output: UsedInOutput(false), 128 | }) 129 | } 130 | 131 | fn register_lifetime(&mut self, entrait_lifetime: EntraitLifetime) -> syn::Lifetime { 132 | let lifetime = entrait_lifetime.lifetime.clone(); 133 | self.et_lifetimes.push(entrait_lifetime); 134 | lifetime 135 | } 136 | 137 | fn broken_lifetime(&self) -> syn::Lifetime { 138 | syn::Lifetime::new("'entrait_broken", proc_macro2::Span::call_site()) 139 | } 140 | } 141 | 142 | impl syn::visit_mut::VisitMut for LifetimeMutVisitor { 143 | fn visit_receiver_mut(&mut self, receiver: &mut syn::Receiver) { 144 | if let Some((_, lifetime)) = &mut receiver.reference { 145 | *lifetime = Some(self.make_lifetime_explicit(lifetime.clone())); 146 | } 147 | syn::visit_mut::visit_receiver_mut(self, receiver); 148 | } 149 | 150 | fn visit_type_reference_mut(&mut self, reference: &mut syn::TypeReference) { 151 | reference.lifetime = Some(self.make_lifetime_explicit(reference.lifetime.clone())); 152 | syn::visit_mut::visit_type_mut(self, reference.elem.as_mut()); 153 | } 154 | 155 | fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) { 156 | *lifetime = self.make_lifetime_explicit(Some(lifetime.clone())) 157 | } 158 | } 159 | 160 | struct ElisionDetector { 161 | receiver_generation: ReceiverGeneration, 162 | current_input: usize, 163 | elided_params: HashSet, 164 | } 165 | 166 | impl ElisionDetector { 167 | fn new(receiver_generation: ReceiverGeneration) -> Self { 168 | Self { 169 | receiver_generation, 170 | current_input: 0, 171 | elided_params: Default::default(), 172 | } 173 | } 174 | 175 | fn detect(&mut self, sig: &mut syn::Signature) { 176 | for (index, input) in sig.inputs.iter_mut().enumerate() { 177 | match self.receiver_generation { 178 | ReceiverGeneration::None => { 179 | self.current_input = index; 180 | self.visit_fn_arg_mut(input); 181 | } 182 | _ => { 183 | if index > 1 { 184 | self.current_input = index - 1; 185 | self.visit_fn_arg_mut(input); 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | impl syn::visit_mut::VisitMut for ElisionDetector { 194 | fn visit_type_reference_mut(&mut self, reference: &mut syn::TypeReference) { 195 | if reference.lifetime.is_none() { 196 | self.elided_params.insert(self.current_input); 197 | } 198 | syn::visit_mut::visit_type_reference_mut(self, reference); 199 | } 200 | 201 | fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) { 202 | if lifetime.ident == "_" { 203 | self.elided_params.insert(self.current_input); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## [0.7.1] - 2024-10-30 10 | ### Added 11 | - #![no_std] 12 | 13 | ## [0.7.0] - 2024-03-27 14 | ### Changed 15 | - Unimock bumped to 0.6. 16 | - Reworked async support. Rust now has native support for async functions in traits, which means that entrait doesn't need to interact with this in a hacky way anymore. 17 | - Minimum Supported Rust Version bumped to 1.75. 18 | - `async` entrait functions will get rewritten to the `fn f(..) -> impl Future` form when appearing in trait definitions, due to the [async_fn_in_trait](https://doc.rust-lang.org/beta/rustc/lints/listing/warn-by-default.html#async-fn-in-trait) warn-by-default lint. 19 | ### Added 20 | - Improved interoperability with the `async_trait` macro, for scenarios where dynamic dispatch-delegation is used in combination with `async`. 21 | - `?Send` argument to the entrait macro, for allowing opt-out of `Send` bounds for the `Future`s generated from `async` trait methods. 22 | ### Removed 23 | - features `boxed-futures`, `use-boxed-futures` and `use-associated-futures`. 24 | 25 | ## [0.6.0] - 2023-10-15 26 | ### Changed 27 | - Unimock bumped to 0.5. 28 | ### Fixed 29 | - Stop adding lifetimes to trait generic params ([#30](https://github.com/audunhalland/entrait/pull/30)) 30 | 31 | ## [0.5.3] - 2023-03-24 32 | ### Changed 33 | - `syn` upgraded to version 2. 34 | 35 | ## [0.5.2] - 2022-12-30 36 | ### Added 37 | - `delegate_by=ref` in favor of `delegate_by = Borrow` (deprecation of the latter). This encourages using `AsRef` instead of `Borrow`. 38 | - `#[entrait(ref)]` on deep delegation impl blocks. 39 | 40 | ## [0.5.1] - 2022-12-30 41 | ### Fixed 42 | - Make examples compile out of the box, by selecting the correct features. 43 | - Entrait methods now correctly mirror attributes like `#[cfg]`. 44 | 45 | ## [0.5.0] - 2022-12-10 46 | ### Changed 47 | - Feature `async-trait` renamed to `boxed-futures`. 48 | - Feature `use-async-trait` renamed to `use-boxed-futures`. 49 | - Feature `use-associated-future` renamed to `use-associated-futures`. 50 | - `unimock` optional dependency upgraded to `0.4`. 51 | - New entrait option `mock_api` to optionally generate a mock setup interface. 52 | ### Removed 53 | - `entrait_impl`. Replaced by `#[entrait] impl TraitImpl for MyType {}`. 54 | - `entrait_dyn_impl`. Replaced by `#[entrait(dyn)] impl TraitImpl for MyType {}`. 55 | 56 | ## [0.4.6] - 2022-08-17 57 | ### Added 58 | - Support for `#[entrait] impl TraitImpl for Type {}` for inverted dependencies. Modules will be deprecated in next release, since this is much cleaner. 59 | ### Changed 60 | - Deprecated the recently added `#[entriat_impl]` and `#[entrait_dyn_impl]` in favor of the impl block syntax, because that idea didn't yet exist at the time of original implementation. 61 | 62 | ## [0.4.5] - 2022-08-11 63 | ### Changed 64 | - Docs: move "Module support" to later in the Introduction section, because it interrupts the original flow of the documentation. 65 | 66 | ## [0.4.4] - 2022-08-11 67 | ### Added 68 | - Dependency inversion support with the `#[entrait(TraitImpl, delegate_by = DelegationTrait)] trait Trait {}` syntax. 69 | - #[inline] attribute on small, delegating methods. 70 | ### Changed 71 | - Make zero-cost futures using a separate macro (`entrait::static_async::async_trait`), comparable to `async_trait`. 72 | 73 | ## [0.4.3] - 2022-08-01 74 | ### Added 75 | - Support for using the entrait attribute on a module. 76 | 77 | ## [0.4.2] - 2022-07-31 78 | ### Added 79 | - `delegate_by = Borrow` option for traits (supports dyn trait leaf dependencies). 80 | ### Fixed 81 | - Fix hygiene problem when a parameter has the same ident as the function. Fix uses a hack that appends an underscore to the trait fn param. 82 | - Improved generic params and where clause generation, should generate some fewer tokens overall. 83 | - Doc: Bring back `Impl` impl block code generation example. 84 | 85 | ## [0.4.1] - 2022-07-25 86 | ### Fixed 87 | - Extract idents from destructured fn params and use those in trait fn signature, given that the ident is unambigous. 88 | ### Changed 89 | - Refactor/optimize internal where clause generator, avoiding syn::parse_quote 90 | 91 | ## [0.4.0] - 2022-07-24 92 | ### Added 93 | - `implementation` as a dependency, to help users getting started. 94 | - `unimock` feature. Enabling the features downstream will generate mocks upstream. 95 | - `entrait_export` macro and `export` option, for exporting optional mocks from libraries. 96 | - `async-trait` feature for adding a re-export of the async-trait crate. 97 | - `use-async-trait` and `use-associated-future` features for global selection of async strategy. 98 | - Support for generic functions. 99 | - Support for entraiting a trait. 100 | ### Changed 101 | - Restructure lib docs. 102 | ### Removed 103 | - Support for parameter-less functions without use of `no_deps`. This is technically 'breaking' but can also be seen as a bugfix. 104 | - Submodule import paths (`entrait::unimock`, etc). This is instead enabled by using features. 105 | ### Fixed 106 | - Destructured fn params in the original function. Entrait will generate a param name to use in the trait. 107 | 108 | ## [0.3.4] - 2022-06-30 109 | ### Added 110 | - More cargo keywords, categories. 111 | 112 | ## [0.3.3] - 2022-06-29 113 | ### Changed 114 | - The implementation of leaf/concrete dependencies now works a bit differently. 115 | Instead of the trait being implemented for some concrete `T` in `Impl`, `T` is made generic, but with a `T: Trait` bound. 116 | Because of that, the trait gets implemented a second time: Directly for the concrete `T`. 117 | This makes it much easier to seamlessly integrate modular apps divided into many crates. 118 | ### Fixed 119 | - Every kind of deps parameter that is not recognized as generic is now legal, and interpreted as being concrete. 120 | 121 | ## [0.3.2] - 2022-06-27 122 | ### Added 123 | - `associated_future` experimental nightly feature, for zero cost futures. 124 | 125 | ## [0.3.1] - 2022-06-22 126 | ### Added 127 | - `no_deps` support. Add this attribute to not interpret the first parameter as a deps parameter. 128 | - default values for config attributes (may skip '= value') 129 | 130 | ## [0.3.0] - 2022-06-03 131 | ### Changed 132 | - Bump unimock to 0.2.0, which removes the need for generic assocated types support 133 | 134 | ## [0.3.0-beta.0] - 2022-05-15 135 | ### Changed 136 | - Improve outputted spans to reflect macro arguments instead of input function 137 | - Bump unimock to next major version (0.2.0) 138 | - Support explicit trait visibility, private/inherited by default 139 | 140 | ### Removed 141 | - Support for `for T` syntax. The implementations are instead automatically registered with the `implementation` crate. 142 | 143 | ## [0.2.1] - 2022-03-13 144 | ### Added 145 | - `mockall=test` + `unimock=test` support 146 | 147 | ## [0.2.0] - 2022-03-13 148 | ### Added 149 | - `unimock` support. 150 | - `cfg_attr(test, ...)` mock support. 151 | ### Removed 152 | - `mock_deps_as`, replaced by `unimock` 153 | - The `entrait_mock_X` procedural macro for multiple-trait-bound mocking. 154 | - The `expand_mock` macro. 155 | 156 | ## [0.1.0] - 2022-03-07 157 | ### Added 158 | - Explicit and opt-in support for `#[async_trait]` with `async_trait = true`. 159 | - Support for `mockall`, with `mockable = true`. 160 | - Support for generating mockall impls for dependencies having multiple trait bounds. 161 | 162 | ### Changed 163 | - Remove all cargo features. Specific features are now passed as key/value arguments to the macro. 164 | - Split crate into a regular lib and a proc macro crate. `macro_rules` macros and other library functions go in the "outer" `entrait` library. 165 | 166 | ## [0.0.2] - 2022-02-24 167 | ### Changed 168 | - Avoid parsing the full `fn` body. The macro only needs to analyze the signature. 169 | 170 | ## [0.0.1] - 2022-02-23 171 | ### Added 172 | - Basic macro with optional async support using `async-trait` 173 | -------------------------------------------------------------------------------- /entrait_macros/src/attributes.rs: -------------------------------------------------------------------------------- 1 | use crate::analyze_generics::TraitFn; 2 | use crate::generics::{self, TraitIndirection}; 3 | use crate::idents::CrateIdents; 4 | use crate::input::FnInputMode; 5 | use crate::opt::{MockApiIdent, Opts}; 6 | use crate::token_util::{comma_sep, push_tokens}; 7 | 8 | use proc_macro2::{Span, TokenStream}; 9 | use quote::ToTokens; 10 | 11 | pub struct Attr

(pub P); 12 | 13 | pub trait IsEmpty { 14 | fn is_empty(&self) -> bool; 15 | } 16 | 17 | impl ToTokens for Attr

{ 18 | fn to_tokens(&self, stream: &mut TokenStream) { 19 | push_tokens!(stream, syn::token::Pound::default()); 20 | syn::token::Bracket::default().surround(stream, |stream| { 21 | push_tokens!(stream, self.0); 22 | }); 23 | } 24 | } 25 | 26 | pub struct ExportGatedAttr<'a, P: ToTokens + IsEmpty> { 27 | pub params: P, 28 | pub opts: &'a Opts, 29 | } 30 | 31 | impl ToTokens for ExportGatedAttr<'_, P> { 32 | fn to_tokens(&self, stream: &mut TokenStream) { 33 | if self.params.is_empty() { 34 | return; 35 | } 36 | push_tokens!(stream, syn::token::Pound::default()); 37 | syn::token::Bracket::default().surround(stream, |stream| { 38 | if self.opts.export_value() { 39 | push_tokens!(stream, self.params); 40 | } else { 41 | push_tokens!(stream, syn::Ident::new("cfg_attr", Span::call_site())); 42 | syn::token::Paren::default().surround(stream, |stream| { 43 | push_tokens!( 44 | stream, 45 | syn::Ident::new("test", Span::call_site()), 46 | syn::token::Comma::default(), 47 | self.params 48 | ); 49 | }); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | pub struct EntraitForTraitParams<'a> { 56 | pub crate_idents: &'a CrateIdents, 57 | } 58 | 59 | impl ToTokens for EntraitForTraitParams<'_> { 60 | fn to_tokens(&self, stream: &mut TokenStream) { 61 | use syn::token::*; 62 | use syn::Ident; 63 | 64 | push_tokens!( 65 | stream, 66 | PathSep::default(), 67 | self.crate_idents.entrait, 68 | PathSep::default(), 69 | self.crate_idents.entrait 70 | ); 71 | Paren::default().surround(stream, |stream| { 72 | push_tokens!( 73 | stream, 74 | Ident::new("unimock", Span::call_site()), 75 | Eq::default(), 76 | syn::LitBool::new(false, Span::call_site()), 77 | Comma::default(), 78 | Ident::new("mockall", Span::call_site()), 79 | Eq::default(), 80 | syn::LitBool::new(false, Span::call_site()) 81 | ); 82 | }); 83 | } 84 | } 85 | 86 | pub struct UnimockAttrParams<'s> { 87 | #[expect(unused)] 88 | pub trait_ident: &'s syn::Ident, 89 | pub mock_api: Option<&'s MockApiIdent>, 90 | pub trait_indirection: TraitIndirection, 91 | pub crate_idents: &'s CrateIdents, 92 | pub trait_fns: &'s [TraitFn], 93 | pub(super) fn_input_mode: &'s FnInputMode<'s>, 94 | pub span: Span, 95 | } 96 | 97 | impl IsEmpty for UnimockAttrParams<'_> { 98 | fn is_empty(&self) -> bool { 99 | matches!(self.trait_indirection, TraitIndirection::Plain) && self.mock_api.is_none() 100 | } 101 | } 102 | 103 | impl ToTokens for UnimockAttrParams<'_> { 104 | fn to_tokens(&self, stream: &mut TokenStream) { 105 | if self.is_empty() { 106 | return; 107 | } 108 | 109 | use syn::token::*; 110 | use syn::Ident; 111 | 112 | let span = self.span; 113 | 114 | push_tokens!( 115 | stream, 116 | PathSep(span), 117 | self.crate_idents.entrait, 118 | PathSep(span), 119 | self.crate_idents.__unimock, 120 | PathSep(span), 121 | self.crate_idents.unimock 122 | ); 123 | 124 | Paren(span).surround(stream, |stream| { 125 | let mut punctuator = comma_sep(stream, span); 126 | 127 | // prefix=::entrait::__unimock 128 | punctuator.push_fn(|stream| { 129 | push_tokens!( 130 | stream, 131 | Ident::new("prefix", span), 132 | Eq(span), 133 | PathSep(span), 134 | self.crate_idents.entrait, 135 | PathSep(span), 136 | self.crate_idents.__unimock 137 | ); 138 | }); 139 | 140 | if let Some(mock_api) = &self.mock_api { 141 | punctuator.push_fn(|stream| { 142 | push_tokens!(stream, Ident::new("api", span), Eq(span)); 143 | 144 | // flatten=[TraitMock] for single-fn entraits 145 | if matches!(self.fn_input_mode, FnInputMode::SingleFn(_)) { 146 | Bracket(span).surround(stream, |stream| push_tokens!(stream, mock_api.0)); 147 | } else { 148 | push_tokens!(stream, mock_api.0); 149 | } 150 | }); 151 | } 152 | 153 | if !matches!(self.fn_input_mode, FnInputMode::RawTrait(_)) { 154 | // unmock_with=[...] 155 | if !self.trait_fns.is_empty() { 156 | punctuator.push_fn(|stream| { 157 | self.unmock_with(stream); 158 | }); 159 | } 160 | } 161 | }); 162 | } 163 | } 164 | 165 | impl UnimockAttrParams<'_> { 166 | fn unmock_with(&self, stream: &mut TokenStream) { 167 | use syn::token::*; 168 | use syn::Ident; 169 | 170 | let span = self.span; 171 | 172 | push_tokens!(stream, Ident::new("unmock_with", span), Eq(span)); 173 | 174 | Bracket(span).surround(stream, |stream| { 175 | let mut punctuator = comma_sep(stream, span); 176 | 177 | for trait_fn in self.trait_fns { 178 | let fn_ident = &trait_fn.sig().ident; 179 | 180 | match &trait_fn.deps { 181 | generics::FnDeps::Generic { .. } => { 182 | punctuator.push(fn_ident); 183 | } 184 | generics::FnDeps::Concrete(_) => { 185 | punctuator.push(Underscore(span)); 186 | } 187 | generics::FnDeps::NoDeps { .. } => { 188 | // fn_ident(a, b, c) 189 | punctuator.push_fn(|stream| { 190 | push_tokens!(stream, fn_ident); 191 | 192 | Paren(span).surround(stream, |stream| { 193 | let mut punctuator = comma_sep(stream, span); 194 | for fn_arg in trait_fn.sig().inputs.iter() { 195 | if let syn::FnArg::Typed(pat_type) = fn_arg { 196 | if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() { 197 | punctuator.push(&pat_ident.ident); 198 | } 199 | } 200 | } 201 | }); 202 | }); 203 | } 204 | } 205 | } 206 | }); 207 | } 208 | } 209 | 210 | pub struct MockallAutomockParams { 211 | pub span: Span, 212 | } 213 | 214 | impl IsEmpty for MockallAutomockParams { 215 | fn is_empty(&self) -> bool { 216 | false 217 | } 218 | } 219 | 220 | impl ToTokens for MockallAutomockParams { 221 | fn to_tokens(&self, stream: &mut TokenStream) { 222 | let span = self.span; 223 | push_tokens!( 224 | stream, 225 | syn::token::PathSep(span), 226 | syn::Ident::new("mockall", span), 227 | syn::token::PathSep(span), 228 | syn::Ident::new("automock", span) 229 | ); 230 | } 231 | } 232 | 233 | pub struct AsyncTraitParams<'a> { 234 | pub crate_idents: &'a CrateIdents, 235 | pub span: Span, 236 | } 237 | 238 | impl ToTokens for AsyncTraitParams<'_> { 239 | fn to_tokens(&self, stream: &mut TokenStream) { 240 | let span = self.span; 241 | push_tokens!( 242 | stream, 243 | syn::token::PathSep(span), 244 | self.crate_idents.entrait, 245 | syn::token::PathSep(span), 246 | syn::Ident::new("__async_trait", span), 247 | syn::token::PathSep(span), 248 | syn::Ident::new("async_trait", span) 249 | ); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /entrait_macros/src/analyze_generics.rs: -------------------------------------------------------------------------------- 1 | use crate::generics::{FnDeps, TraitDependencyMode, TraitGenerics}; 2 | use crate::idents::{CrateIdents, GenericIdents}; 3 | use crate::input::FnInputMode; 4 | use crate::opt::Opts; 5 | use crate::signature::ImplReceiverKind; 6 | use crate::signature::{converter::SignatureConverter, EntraitSignature, InputSig}; 7 | use crate::token_util::TokenPair; 8 | 9 | use proc_macro2::Span; 10 | use syn::spanned::Spanned; 11 | 12 | #[derive(Clone)] 13 | pub struct TraitFn { 14 | pub deps: FnDeps, 15 | pub attrs: Vec, 16 | pub entrait_sig: EntraitSignature, 17 | pub originally_async: bool, 18 | } 19 | 20 | impl TraitFn { 21 | pub fn sig(&self) -> &syn::Signature { 22 | &self.entrait_sig.sig 23 | } 24 | 25 | pub fn opt_dot_await(&self, span: Span) -> Option { 26 | if self.originally_async { 27 | Some(TokenPair(syn::token::Dot(span), syn::token::Await(span))) 28 | } else { 29 | None 30 | } 31 | } 32 | } 33 | 34 | pub struct TraitFnAnalyzer<'s> { 35 | pub impl_receiver_kind: ImplReceiverKind, 36 | pub trait_span: Span, 37 | pub crate_idents: &'s CrateIdents, 38 | pub opts: &'s Opts, 39 | } 40 | 41 | impl TraitFnAnalyzer<'_> { 42 | pub fn analyze( 43 | self, 44 | input_sig: InputSig<'_>, 45 | analyzer: &mut GenericsAnalyzer, 46 | ) -> syn::Result { 47 | let deps = analyzer.analyze_fn_deps(input_sig, self.opts)?; 48 | let entrait_sig = SignatureConverter { 49 | crate_idents: self.crate_idents, 50 | trait_span: self.trait_span, 51 | opts: self.opts, 52 | input_sig, 53 | deps: &deps, 54 | impl_receiver_kind: self.impl_receiver_kind, 55 | } 56 | .convert_fn_to_trait_fn(); 57 | Ok(TraitFn { 58 | deps, 59 | attrs: vec![], 60 | entrait_sig, 61 | originally_async: input_sig.asyncness.is_some(), 62 | }) 63 | } 64 | } 65 | 66 | pub(super) fn detect_trait_dependency_mode<'t, 'c>( 67 | input_mode: &FnInputMode, 68 | trait_fns: &'t [TraitFn], 69 | crate_idents: &'c CrateIdents, 70 | span: proc_macro2::Span, 71 | ) -> syn::Result> { 72 | for trait_fn in trait_fns { 73 | if let FnDeps::Concrete(ty) = &trait_fn.deps { 74 | return match input_mode { 75 | FnInputMode::SingleFn(_) => Ok(TraitDependencyMode::Concrete(ty.as_ref())), 76 | FnInputMode::Module(_) => Err(syn::Error::new( 77 | ty.span(), 78 | "Using concrete dependencies in a module is an anti-pattern. Instead, write a trait manually, use the #[entrait] attribute on it, and implement it for your application type", 79 | )), 80 | FnInputMode::ImplBlock(_) => Err(syn::Error::new( 81 | ty.span(), 82 | "Cannot (yet) use concrete dependency in an impl block" 83 | )), 84 | FnInputMode::RawTrait(_) => panic!("Should not detect dependencies for this input mode") 85 | }; 86 | } 87 | } 88 | 89 | Ok(TraitDependencyMode::Generic(GenericIdents::new( 90 | crate_idents, 91 | span, 92 | ))) 93 | } 94 | 95 | pub struct GenericsAnalyzer { 96 | trait_generics: TraitGenerics, 97 | } 98 | 99 | impl GenericsAnalyzer { 100 | pub fn new() -> Self { 101 | Self { 102 | trait_generics: TraitGenerics { 103 | params: Default::default(), 104 | where_predicates: Default::default(), 105 | }, 106 | } 107 | } 108 | 109 | pub fn into_trait_generics(self) -> TraitGenerics { 110 | self.trait_generics 111 | } 112 | 113 | pub fn analyze_fn_deps(&mut self, input_sig: InputSig<'_>, opts: &Opts) -> syn::Result { 114 | if opts.no_deps_value() { 115 | return self.deps_with_generics(FnDeps::NoDeps, &input_sig.generics); 116 | } 117 | 118 | let first_input = 119 | match input_sig.inputs.first() { 120 | Some(fn_arg) => fn_arg, 121 | None => return Err(syn::Error::new( 122 | input_sig.ident.span(), 123 | "Function must have a dependency 'receiver' as its first parameter. Pass `no_deps` to entrait to disable dependency injection.", 124 | )), 125 | }; 126 | 127 | let pat_type = match first_input { 128 | syn::FnArg::Typed(pat_type) => pat_type, 129 | syn::FnArg::Receiver(_) => { 130 | return Err(syn::Error::new( 131 | first_input.span(), 132 | "Function cannot have a self receiver", 133 | )) 134 | } 135 | }; 136 | 137 | self.extract_deps_from_type(input_sig, pat_type.ty.as_ref()) 138 | } 139 | 140 | fn extract_deps_from_type( 141 | &mut self, 142 | input_sig: InputSig<'_>, 143 | ty: &syn::Type, 144 | ) -> syn::Result { 145 | match ty { 146 | syn::Type::ImplTrait(type_impl_trait) => { 147 | // Simple case, bounds are actually inline, no lookup necessary 148 | self.deps_with_generics( 149 | FnDeps::Generic { 150 | generic_param: None, 151 | trait_bounds: extract_trait_bounds(&type_impl_trait.bounds), 152 | }, 153 | &input_sig.generics, 154 | ) 155 | } 156 | syn::Type::Path(type_path) => { 157 | // Type path. Should be defined as a generic parameter. 158 | if type_path.qself.is_some() { 159 | return Err(syn::Error::new(type_path.span(), "No self allowed")); 160 | } 161 | if type_path.path.leading_colon.is_some() { 162 | return Err(syn::Error::new( 163 | type_path.span(), 164 | "No leading colon allowed", 165 | )); 166 | } 167 | if type_path.path.segments.len() != 1 { 168 | return self.deps_with_generics( 169 | FnDeps::Concrete(Box::new(ty.clone())), 170 | &input_sig.generics, 171 | ); 172 | } 173 | 174 | let first_segment = type_path.path.segments.first().unwrap(); 175 | 176 | match self.find_deps_generic_bounds(input_sig, &first_segment.ident) { 177 | Some(generics) => Ok(generics), 178 | None => self.deps_with_generics( 179 | FnDeps::Concrete(Box::new(ty.clone())), 180 | &input_sig.generics, 181 | ), 182 | } 183 | } 184 | syn::Type::Reference(type_reference) => { 185 | self.extract_deps_from_type(input_sig, type_reference.elem.as_ref()) 186 | } 187 | syn::Type::Paren(paren) => self.extract_deps_from_type(input_sig, paren.elem.as_ref()), 188 | ty => { 189 | self.deps_with_generics(FnDeps::Concrete(Box::new(ty.clone())), &input_sig.generics) 190 | } 191 | } 192 | } 193 | 194 | fn find_deps_generic_bounds( 195 | &mut self, 196 | input_sig: InputSig<'_>, 197 | generic_param_ident: &syn::Ident, 198 | ) -> Option { 199 | let generics = &input_sig.generics; 200 | let generic_params = &generics.params; 201 | 202 | let (matching_index, matching_type_param) = generic_params 203 | .into_iter() 204 | .enumerate() 205 | .find_map(|(index, param)| match param { 206 | syn::GenericParam::Type(type_param) => { 207 | if &type_param.ident == generic_param_ident { 208 | Some((index, type_param)) 209 | } else { 210 | None 211 | } 212 | } 213 | _ => None, 214 | })?; 215 | 216 | for (index, param) in generic_params.iter().enumerate() { 217 | if index != matching_index && !(matches!(param, &syn::GenericParam::Lifetime(_))) { 218 | self.trait_generics.params.push(param.clone()); 219 | } 220 | } 221 | 222 | // Extract "direct" bounds, not from where clause 223 | let mut deps_trait_bounds = extract_trait_bounds(&matching_type_param.bounds); 224 | 225 | if let Some(where_clause) = &generics.where_clause { 226 | for predicate in &where_clause.predicates { 227 | match predicate { 228 | syn::WherePredicate::Type(predicate_type) => match &predicate_type.bounded_ty { 229 | syn::Type::Path(type_path) => { 230 | if type_path.qself.is_some() || type_path.path.leading_colon.is_some() { 231 | self.trait_generics.where_predicates.push(predicate.clone()); 232 | continue; 233 | } 234 | if type_path.path.segments.len() != 1 { 235 | self.trait_generics.where_predicates.push(predicate.clone()); 236 | continue; 237 | } 238 | let first_segment = type_path.path.segments.first().unwrap(); 239 | 240 | if &first_segment.ident == generic_param_ident { 241 | let where_paths = extract_trait_bounds(&predicate_type.bounds); 242 | 243 | deps_trait_bounds.extend(where_paths); 244 | } 245 | } 246 | _ => { 247 | self.trait_generics.where_predicates.push(predicate.clone()); 248 | } 249 | }, 250 | _ => { 251 | self.trait_generics.where_predicates.push(predicate.clone()); 252 | } 253 | } 254 | } 255 | }; 256 | 257 | Some(FnDeps::Generic { 258 | generic_param: Some(generic_param_ident.clone()), 259 | trait_bounds: deps_trait_bounds, 260 | }) 261 | } 262 | 263 | fn deps_with_generics( 264 | &mut self, 265 | deps: FnDeps, 266 | generics: &syn::Generics, 267 | ) -> syn::Result { 268 | for param in &generics.params { 269 | match param { 270 | syn::GenericParam::Type(_) => { 271 | self.trait_generics.params.push(param.clone()); 272 | } 273 | syn::GenericParam::Const(_) => { 274 | self.trait_generics.params.push(param.clone()); 275 | } 276 | syn::GenericParam::Lifetime(_) => {} 277 | } 278 | } 279 | 280 | if let Some(where_clause) = &generics.where_clause { 281 | for predicate in &where_clause.predicates { 282 | self.trait_generics.where_predicates.push(predicate.clone()); 283 | } 284 | } 285 | 286 | Ok(deps) 287 | } 288 | } 289 | 290 | fn extract_trait_bounds( 291 | bounds: &syn::punctuated::Punctuated, 292 | ) -> Vec { 293 | bounds.iter().cloned().collect() 294 | } 295 | -------------------------------------------------------------------------------- /entrait_macros/src/generics.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | use crate::{ 4 | analyze_generics::TraitFn, 5 | idents::GenericIdents, 6 | token_util::{push_tokens, EmptyToken, Punctuator, TokenPair}, 7 | }; 8 | 9 | #[derive(Clone)] 10 | pub enum ImplIndirection<'s> { 11 | None, 12 | Static { ty: &'s syn::Type }, 13 | Dynamic { ty: &'s syn::Type }, 14 | } 15 | 16 | #[derive(Clone, Copy)] 17 | pub enum TraitIndirection { 18 | /// Normal entrait/fn, entrait/mod etc 19 | Plain, 20 | /// Trait indirection is the original entraited trait 21 | Trait, 22 | /// The static "impl" trait of an entraited trait 23 | StaticImpl, 24 | /// The dynamic/borrow "impl" trait of an entraited trait 25 | DynamicImpl, 26 | } 27 | 28 | #[derive(Clone, Copy)] 29 | pub struct TakesSelfByValue(pub bool); 30 | 31 | pub fn has_any_self_by_value<'s>( 32 | mut signatures: impl Iterator, 33 | ) -> TakesSelfByValue { 34 | TakesSelfByValue(signatures.any(|sig| match sig.inputs.first() { 35 | Some(syn::FnArg::Receiver(receiver)) => receiver.reference.is_none(), 36 | _ => false, 37 | })) 38 | } 39 | 40 | #[derive(Clone)] 41 | pub enum FnDeps { 42 | Generic { 43 | generic_param: Option, 44 | trait_bounds: Vec, 45 | }, 46 | Concrete(Box), 47 | NoDeps, 48 | } 49 | 50 | pub enum TraitDependencyMode<'t, 'c> { 51 | Generic(GenericIdents<'c>), 52 | Concrete(&'t syn::Type), 53 | } 54 | 55 | #[derive(Clone)] 56 | pub struct TraitGenerics { 57 | pub params: syn::punctuated::Punctuated, 58 | pub where_predicates: syn::punctuated::Punctuated, 59 | } 60 | 61 | impl TraitGenerics { 62 | pub fn trait_params(&self) -> ParamsGenerator<'_> { 63 | ParamsGenerator { 64 | params: &self.params, 65 | impl_t: None, 66 | takes_self_by_value: TakesSelfByValue(false), 67 | } 68 | } 69 | 70 | pub fn trait_where_clause(&self) -> TraitWhereClauseGenerator<'_> { 71 | TraitWhereClauseGenerator { 72 | where_predicates: &self.where_predicates, 73 | } 74 | } 75 | 76 | pub fn impl_params<'i>( 77 | &'i self, 78 | trait_dependency_mode: &'i TraitDependencyMode<'i, '_>, 79 | takes_self_by_value: TakesSelfByValue, 80 | ) -> ParamsGenerator<'i> { 81 | ParamsGenerator { 82 | params: &self.params, 83 | impl_t: match trait_dependency_mode { 84 | TraitDependencyMode::Generic(idents) => Some(&idents.impl_t), 85 | TraitDependencyMode::Concrete(_) => None, 86 | }, 87 | takes_self_by_value, 88 | } 89 | } 90 | 91 | pub fn impl_params_from_idents<'i>( 92 | &'i self, 93 | idents: &'i GenericIdents, 94 | takes_self_by_value: TakesSelfByValue, 95 | ) -> ParamsGenerator<'i> { 96 | ParamsGenerator { 97 | params: &self.params, 98 | impl_t: Some(&idents.impl_t), 99 | takes_self_by_value, 100 | } 101 | } 102 | 103 | pub fn impl_where_clause<'g, 's, 'c>( 104 | &'g self, 105 | trait_fns: &'s [TraitFn], 106 | trait_dependency_mode: &'s TraitDependencyMode<'s, 'c>, 107 | impl_indirection: &'s ImplIndirection, 108 | span: proc_macro2::Span, 109 | ) -> ImplWhereClauseGenerator<'g, 's, 'c> { 110 | ImplWhereClauseGenerator { 111 | trait_where_predicates: &self.where_predicates, 112 | trait_dependency_mode, 113 | impl_indirection, 114 | trait_fns, 115 | span, 116 | } 117 | } 118 | 119 | pub fn arguments<'s>( 120 | &'s self, 121 | impl_indirection: &'s ImplIndirection, 122 | ) -> ArgumentsGenerator<'s> { 123 | ArgumentsGenerator { 124 | params: &self.params, 125 | impl_indirection, 126 | } 127 | } 128 | } 129 | 130 | impl<'c> GenericIdents<'c> { 131 | /// `::entrait::Impl` 132 | pub fn impl_path<'s>(&'s self, span: proc_macro2::Span) -> ImplPath<'s, 'c> { 133 | ImplPath(self, span) 134 | } 135 | } 136 | 137 | /// `::entrait::Impl` 138 | pub struct ImplPath<'g, 'c>(&'g GenericIdents<'c>, proc_macro2::Span); 139 | 140 | impl quote::ToTokens for ImplPath<'_, '_> { 141 | fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { 142 | let span = self.1; 143 | 144 | push_tokens!( 145 | stream, 146 | syn::token::PathSep(span), 147 | self.0.crate_idents.entrait, 148 | syn::token::PathSep(span), 149 | self.0.impl_self, 150 | syn::token::Lt(span), 151 | self.0.impl_t, 152 | syn::token::Gt(span) 153 | ); 154 | } 155 | } 156 | 157 | // Params as in impl<..Param> 158 | pub struct ParamsGenerator<'g> { 159 | params: &'g syn::punctuated::Punctuated, 160 | impl_t: Option<&'g syn::Ident>, 161 | takes_self_by_value: TakesSelfByValue, 162 | } 163 | 164 | impl quote::ToTokens for ParamsGenerator<'_> { 165 | fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { 166 | let mut punctuator = Punctuator::new( 167 | stream, 168 | syn::token::Lt::default(), 169 | syn::token::Comma::default(), 170 | syn::token::Gt::default(), 171 | ); 172 | 173 | if let Some(impl_t) = &self.impl_t { 174 | punctuator.push_fn(|stream| { 175 | push_tokens!( 176 | stream, 177 | impl_t, 178 | syn::token::Colon::default(), 179 | syn::Ident::new("Sync", proc_macro2::Span::call_site()) 180 | ); 181 | 182 | if self.takes_self_by_value.0 { 183 | push_tokens!( 184 | stream, 185 | syn::token::Plus::default(), 186 | // In case T is not a reference, it has to be Send 187 | syn::Ident::new("Send", proc_macro2::Span::call_site()) 188 | ); 189 | } 190 | 191 | // if self.use_associated_future.0 { 192 | if true { 193 | push_tokens!( 194 | stream, 195 | syn::token::Plus::default(), 196 | // Deps must be 'static for zero-cost futures to work 197 | syn::Lifetime::new("'static", proc_macro2::Span::call_site()) 198 | ); 199 | } 200 | }); 201 | } 202 | 203 | for param in self.params { 204 | punctuator.push(param); 205 | } 206 | } 207 | } 208 | 209 | // Args as in impl<..Param> T for U<..Arg> 210 | pub struct ArgumentsGenerator<'g> { 211 | params: &'g syn::punctuated::Punctuated, 212 | impl_indirection: &'g ImplIndirection<'g>, 213 | } 214 | 215 | impl quote::ToTokens for ArgumentsGenerator<'_> { 216 | fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { 217 | let mut punctuator = Punctuator::new( 218 | stream, 219 | syn::token::Lt::default(), 220 | syn::token::Comma::default(), 221 | syn::token::Gt::default(), 222 | ); 223 | 224 | if matches!( 225 | &self.impl_indirection, 226 | ImplIndirection::Static { .. } | ImplIndirection::Dynamic { .. } 227 | ) { 228 | punctuator.push(syn::Ident::new("EntraitT", proc_macro2::Span::call_site())); 229 | } 230 | 231 | for pair in self.params.pairs() { 232 | match pair.value() { 233 | syn::GenericParam::Type(type_param) => { 234 | punctuator.push(&type_param.ident); 235 | } 236 | syn::GenericParam::Lifetime(lifetime_def) => { 237 | punctuator.push(&lifetime_def.lifetime); 238 | } 239 | syn::GenericParam::Const(const_param) => { 240 | punctuator.push(&const_param.ident); 241 | } 242 | } 243 | } 244 | } 245 | } 246 | 247 | pub struct TraitWhereClauseGenerator<'g> { 248 | where_predicates: &'g syn::punctuated::Punctuated, 249 | } 250 | 251 | impl quote::ToTokens for TraitWhereClauseGenerator<'_> { 252 | fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { 253 | if self.where_predicates.is_empty() { 254 | return; 255 | } 256 | 257 | push_tokens!(stream, syn::token::Where::default()); 258 | 259 | for pair in self.where_predicates.pairs() { 260 | push_tokens!(stream, pair); 261 | } 262 | } 263 | } 264 | 265 | pub struct ImplWhereClauseGenerator<'g, 's, 'c> { 266 | trait_where_predicates: &'g syn::punctuated::Punctuated, 267 | trait_dependency_mode: &'s TraitDependencyMode<'s, 'c>, 268 | impl_indirection: &'s ImplIndirection<'s>, 269 | trait_fns: &'s [TraitFn], 270 | span: proc_macro2::Span, 271 | } 272 | 273 | impl quote::ToTokens for ImplWhereClauseGenerator<'_, '_, '_> { 274 | fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { 275 | let mut punctuator = Punctuator::new( 276 | stream, 277 | syn::token::Where(self.span), 278 | syn::token::Comma(self.span), 279 | EmptyToken, 280 | ); 281 | 282 | // The where clause looks quite different depending on what kind of Deps is used in the function. 283 | match &self.trait_dependency_mode { 284 | TraitDependencyMode::Generic(generic_idents) => { 285 | // Impl bounds 286 | 287 | let has_bounds = self.trait_fns.iter().any(|trait_fn| match &trait_fn.deps { 288 | FnDeps::Generic { trait_bounds, .. } => !trait_bounds.is_empty(), 289 | _ => false, 290 | }); 291 | 292 | if has_bounds { 293 | punctuator.push_fn(|stream| match self.impl_indirection { 294 | ImplIndirection::None => { 295 | push_impl_t_bounds( 296 | stream, 297 | syn::token::SelfType(self.span), 298 | self.trait_fns, 299 | self.span, 300 | ); 301 | } 302 | ImplIndirection::Static { .. } | ImplIndirection::Dynamic { .. } => { 303 | push_impl_t_bounds( 304 | stream, 305 | generic_idents.impl_path(self.span), 306 | self.trait_fns, 307 | self.span, 308 | ); 309 | } 310 | }); 311 | } 312 | } 313 | TraitDependencyMode::Concrete(_) => { 314 | // NOTE: the impl for Impl is generated by invoking #[entrait] on the trait(!), 315 | // So we need only one impl here: for the path (the `T` in `Impl`). 316 | } 317 | }; 318 | 319 | for predicate in self.trait_where_predicates { 320 | punctuator.push(predicate); 321 | } 322 | } 323 | } 324 | 325 | fn push_impl_t_bounds( 326 | stream: &mut TokenStream, 327 | bound_param: impl quote::ToTokens, 328 | trait_fns: &[TraitFn], 329 | span: proc_macro2::Span, 330 | ) { 331 | let mut bound_punctuator = Punctuator::new( 332 | stream, 333 | TokenPair(bound_param, syn::token::Colon(span)), 334 | syn::token::Plus(span), 335 | EmptyToken, 336 | ); 337 | 338 | for trait_fn in trait_fns { 339 | if let FnDeps::Generic { trait_bounds, .. } = &trait_fn.deps { 340 | for bound in trait_bounds { 341 | bound_punctuator.push(bound); 342 | } 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /entrait_macros/src/input.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! inputs to procedural macros 3 | //! 4 | //! 5 | //! 6 | 7 | use crate::{signature::InputSig, token_util::push_tokens}; 8 | 9 | use proc_macro2::TokenStream; 10 | use quote::ToTokens; 11 | use syn::{ 12 | parse::{Parse, ParseStream}, 13 | spanned::Spanned, 14 | }; 15 | 16 | #[expect(unused)] 17 | pub enum FnInputMode<'a> { 18 | SingleFn(&'a syn::Ident), 19 | Module(&'a syn::Ident), 20 | ImplBlock(&'a syn::Type), 21 | RawTrait(LiteralAttrs<'a>), 22 | } 23 | 24 | pub struct LiteralAttrs<'a>(pub &'a [syn::Attribute]); 25 | 26 | impl ToTokens for LiteralAttrs<'_> { 27 | fn to_tokens(&self, stream: &mut TokenStream) { 28 | for attr in self.0 { 29 | attr.to_tokens(stream); 30 | } 31 | } 32 | } 33 | 34 | pub enum Input { 35 | Fn(InputFn), 36 | Trait(syn::ItemTrait), 37 | Mod(InputMod), 38 | Impl(InputImpl), 39 | } 40 | 41 | impl Parse for Input { 42 | fn parse(input: ParseStream) -> syn::Result { 43 | let attrs = input.call(syn::Attribute::parse_outer)?; 44 | let vis = input.parse()?; 45 | 46 | let unsafety: Option = input.parse()?; 47 | let auto_token: Option = input.parse()?; 48 | 49 | // BUG (In theory): missing and "auto" traits 50 | if input.peek(syn::token::Trait) { 51 | let item_trait: syn::ItemTrait = input.parse()?; 52 | 53 | Ok(Input::Trait(syn::ItemTrait { 54 | attrs, 55 | vis, 56 | unsafety, 57 | auto_token, 58 | ..item_trait 59 | })) 60 | } else if input.peek(syn::token::Impl) { 61 | disallow_token(auto_token)?; 62 | Ok(Input::Impl(parse_impl(attrs, unsafety, input)?)) 63 | } else if input.peek(syn::token::Mod) { 64 | disallow_token(unsafety)?; 65 | disallow_token(auto_token)?; 66 | Ok(Input::Mod(parse_mod(attrs, vis, input)?)) 67 | } else { 68 | let fn_sig: syn::Signature = input.parse()?; 69 | let fn_body = input.parse()?; 70 | 71 | Ok(Input::Fn(InputFn { 72 | fn_attrs: attrs, 73 | fn_vis: vis, 74 | fn_sig, 75 | fn_body, 76 | })) 77 | } 78 | } 79 | } 80 | 81 | pub struct InputFn { 82 | pub fn_attrs: Vec, 83 | pub fn_vis: syn::Visibility, 84 | pub fn_sig: syn::Signature, 85 | // don't try to parse fn_body, just pass through the tokens: 86 | pub fn_body: proc_macro2::TokenStream, 87 | } 88 | 89 | impl InputFn { 90 | pub fn input_sig(&self) -> InputSig<'_> { 91 | InputSig::new(&self.fn_sig) 92 | } 93 | } 94 | 95 | pub struct InputMod { 96 | pub attrs: Vec, 97 | pub vis: syn::Visibility, 98 | pub mod_token: syn::token::Mod, 99 | pub ident: syn::Ident, 100 | pub brace_token: syn::token::Brace, 101 | pub items: Vec, 102 | } 103 | 104 | impl Parse for InputMod { 105 | fn parse(input: ParseStream) -> syn::Result { 106 | let attrs = input.call(syn::Attribute::parse_outer)?; 107 | let vis = input.parse()?; 108 | parse_mod(attrs, vis, input) 109 | } 110 | } 111 | 112 | impl ToTokens for InputMod { 113 | fn to_tokens(&self, stream: &mut TokenStream) { 114 | for attr in &self.attrs { 115 | push_tokens!(stream, attr); 116 | } 117 | push_tokens!(stream, self.vis, self.mod_token, self.ident); 118 | self.brace_token.surround(stream, |stream| { 119 | for item in &self.items { 120 | item.to_tokens(stream); 121 | } 122 | }); 123 | } 124 | } 125 | 126 | pub enum ModItem { 127 | PubFn(Box), 128 | Unknown(ItemUnknown), 129 | } 130 | 131 | impl ModItem { 132 | // We include all functions that have a visibility keyword into the trait 133 | pub fn filter_pub_fn(&self) -> Option<&InputFn> { 134 | match self { 135 | Self::PubFn(input_fn) => Some(input_fn), 136 | _ => None, 137 | } 138 | } 139 | } 140 | 141 | impl ToTokens for ModItem { 142 | fn to_tokens(&self, stream: &mut TokenStream) { 143 | match self { 144 | ModItem::PubFn(input_fn) => { 145 | let InputFn { 146 | fn_attrs, 147 | fn_vis, 148 | fn_sig, 149 | fn_body, 150 | } = input_fn.as_ref(); 151 | for attr in fn_attrs { 152 | push_tokens!(stream, attr); 153 | } 154 | push_tokens!(stream, fn_vis, fn_sig, fn_body); 155 | } 156 | ModItem::Unknown(unknown) => { 157 | unknown.to_tokens(stream); 158 | } 159 | } 160 | } 161 | } 162 | 163 | #[expect(unused)] 164 | pub struct DeriveImplTraitPath(pub syn::Path); 165 | 166 | /// An impl block 167 | /// Note: No support for generics 168 | pub struct InputImpl { 169 | pub attrs: Vec, 170 | pub unsafety: Option, 171 | pub impl_token: syn::token::Impl, 172 | pub trait_path: syn::Path, 173 | #[expect(unused)] 174 | pub for_token: syn::token::For, 175 | pub self_ty: syn::Type, 176 | #[expect(unused)] 177 | pub brace_token: syn::token::Brace, 178 | pub items: Vec, 179 | } 180 | 181 | pub enum ImplItem { 182 | Fn(Box), 183 | Unknown(ItemUnknown), 184 | } 185 | 186 | impl ImplItem { 187 | // We include all functions that have a visibility keyword into the trait 188 | pub fn filter_fn(&self) -> Option<&InputFn> { 189 | match self { 190 | Self::Fn(input_fn) => Some(input_fn), 191 | _ => None, 192 | } 193 | } 194 | } 195 | 196 | impl ToTokens for ImplItem { 197 | fn to_tokens(&self, stream: &mut TokenStream) { 198 | match self { 199 | ImplItem::Fn(input_fn) => { 200 | let InputFn { 201 | fn_attrs, 202 | fn_vis, 203 | fn_sig, 204 | fn_body, 205 | } = input_fn.as_ref(); 206 | for attr in fn_attrs { 207 | push_tokens!(stream, attr); 208 | } 209 | push_tokens!(stream, fn_vis, fn_sig, fn_body); 210 | } 211 | ImplItem::Unknown(unknown) => { 212 | unknown.to_tokens(stream); 213 | } 214 | } 215 | } 216 | } 217 | 218 | pub struct ItemUnknown { 219 | attrs: Vec, 220 | vis: syn::Visibility, 221 | tokens: TokenStream, 222 | } 223 | 224 | impl ToTokens for ItemUnknown { 225 | fn to_tokens(&self, stream: &mut TokenStream) { 226 | for attr in &self.attrs { 227 | attr.to_tokens(stream); 228 | } 229 | push_tokens!(stream, self.vis, self.tokens); 230 | } 231 | } 232 | 233 | fn parse_mod( 234 | attrs: Vec, 235 | vis: syn::Visibility, 236 | input: ParseStream, 237 | ) -> syn::Result { 238 | let mod_token = input.parse()?; 239 | let ident = input.parse()?; 240 | 241 | let lookahead = input.lookahead1(); 242 | if lookahead.peek(syn::token::Brace) { 243 | let content; 244 | let brace_token = syn::braced!(content in input); 245 | 246 | let mut items = vec![]; 247 | 248 | while !content.is_empty() { 249 | items.push(content.parse()?); 250 | } 251 | 252 | Ok(InputMod { 253 | attrs, 254 | vis, 255 | mod_token, 256 | ident, 257 | brace_token, 258 | items, 259 | }) 260 | } else { 261 | Err(lookahead.error()) 262 | } 263 | } 264 | 265 | impl Parse for ModItem { 266 | fn parse(input: ParseStream) -> syn::Result { 267 | let attrs = input.call(syn::Attribute::parse_outer)?; 268 | 269 | let vis: syn::Visibility = input.parse()?; 270 | let unknown = input.fork(); 271 | 272 | if peek_pub_fn(input, &vis) { 273 | let sig: syn::Signature = input.parse()?; 274 | if input.peek(syn::token::Semi) { 275 | let _ = input.parse::()?; 276 | Ok(ModItem::Unknown(ItemUnknown { 277 | attrs, 278 | vis, 279 | tokens: verbatim_between(unknown, input), 280 | })) 281 | } else { 282 | let fn_body = parse_matched_braces_or_ending_semi(input)?; 283 | Ok(ModItem::PubFn(Box::new(InputFn { 284 | fn_attrs: attrs, 285 | fn_vis: vis, 286 | fn_sig: sig, 287 | fn_body, 288 | }))) 289 | } 290 | } else { 291 | let tokens = parse_matched_braces_or_ending_semi(input)?; 292 | Ok(ModItem::Unknown(ItemUnknown { attrs, vis, tokens })) 293 | } 294 | } 295 | } 296 | 297 | fn parse_impl( 298 | attrs: Vec, 299 | unsafety: Option, 300 | input: ParseStream, 301 | ) -> syn::Result { 302 | let impl_token = input.parse()?; 303 | let trait_path = input.parse()?; 304 | let for_token = input.parse()?; 305 | let self_ty = input.parse()?; 306 | 307 | let lookahead = input.lookahead1(); 308 | if lookahead.peek(syn::token::Brace) { 309 | let content; 310 | let brace_token = syn::braced!(content in input); 311 | 312 | let mut items = vec![]; 313 | 314 | while !content.is_empty() { 315 | items.push(content.parse()?); 316 | } 317 | 318 | Ok(InputImpl { 319 | attrs, 320 | unsafety, 321 | impl_token, 322 | trait_path, 323 | for_token, 324 | self_ty, 325 | brace_token, 326 | items, 327 | }) 328 | } else { 329 | Err(lookahead.error()) 330 | } 331 | } 332 | 333 | impl Parse for ImplItem { 334 | fn parse(input: ParseStream) -> syn::Result { 335 | let attrs = input.call(syn::Attribute::parse_outer)?; 336 | 337 | let vis: syn::Visibility = input.parse()?; 338 | let unknown = input.fork(); 339 | 340 | if peek_fn(input) { 341 | let sig: syn::Signature = input.parse()?; 342 | if input.peek(syn::token::Semi) { 343 | let _ = input.parse::()?; 344 | Ok(ImplItem::Unknown(ItemUnknown { 345 | attrs, 346 | vis, 347 | tokens: verbatim_between(unknown, input), 348 | })) 349 | } else { 350 | let fn_body = parse_matched_braces_or_ending_semi(input)?; 351 | Ok(ImplItem::Fn(Box::new(InputFn { 352 | fn_attrs: attrs, 353 | fn_vis: vis, 354 | fn_sig: sig, 355 | fn_body, 356 | }))) 357 | } 358 | } else { 359 | let tokens = parse_matched_braces_or_ending_semi(input)?; 360 | Ok(ImplItem::Unknown(ItemUnknown { attrs, vis, tokens })) 361 | } 362 | } 363 | } 364 | 365 | impl Parse for DeriveImplTraitPath { 366 | fn parse(input: ParseStream) -> syn::Result { 367 | let content; 368 | let _ = syn::parenthesized!(content in input); 369 | 370 | let path = content.parse()?; 371 | 372 | Ok(DeriveImplTraitPath(path)) 373 | } 374 | } 375 | 376 | fn peek_pub_fn(input: ParseStream, vis: &syn::Visibility) -> bool { 377 | if let syn::Visibility::Inherited = vis { 378 | // 'private' functions aren't interesting 379 | return false; 380 | } 381 | peek_fn(input) 382 | } 383 | 384 | fn peek_fn(input: ParseStream) -> bool { 385 | if input.peek(syn::token::Fn) { 386 | return true; 387 | } 388 | 389 | let fork = input.fork(); 390 | fork.parse::>().is_ok() 391 | && fork.parse::>().is_ok() 392 | && fork.parse::>().is_ok() 393 | && fork.parse::>().is_ok() 394 | && fork.peek(syn::token::Fn) 395 | } 396 | 397 | fn verbatim_between<'a>(begin: syn::parse::ParseBuffer<'a>, end: ParseStream<'a>) -> TokenStream { 398 | let end = end.cursor(); 399 | let mut cursor = begin.cursor(); 400 | let mut tokens = TokenStream::new(); 401 | while cursor != end { 402 | let (tt, next) = cursor.token_tree().unwrap(); 403 | tokens.extend(std::iter::once(tt)); 404 | cursor = next; 405 | } 406 | tokens 407 | } 408 | 409 | fn parse_matched_braces_or_ending_semi(input: ParseStream) -> syn::Result { 410 | let mut tokens = input.step(|cursor| { 411 | let mut tokens = TokenStream::new(); 412 | 413 | use proc_macro2::Delimiter; 414 | use proc_macro2::TokenTree; 415 | 416 | let mut rest = *cursor; 417 | 418 | while let Some((tt, next)) = rest.token_tree() { 419 | match &tt { 420 | TokenTree::Group(group) => { 421 | let is_brace = group.delimiter() == Delimiter::Brace; 422 | tokens.extend(std::iter::once(tt)); 423 | if is_brace { 424 | return Ok((tokens, next)); 425 | } 426 | } 427 | TokenTree::Punct(punct) => { 428 | let is_semi = punct.as_char() == ';'; 429 | tokens.extend(std::iter::once(tt)); 430 | if is_semi { 431 | return Ok((tokens, next)); 432 | } 433 | } 434 | _ => { 435 | tokens.extend(std::iter::once(tt)); 436 | } 437 | } 438 | rest = next; 439 | } 440 | Err(syn::Error::new( 441 | proc_macro2::Span::call_site(), 442 | "Read past the end", 443 | )) 444 | })?; 445 | 446 | while input.peek(syn::token::Semi) { 447 | let semi = input.parse::()?; 448 | semi.to_tokens(&mut tokens); 449 | } 450 | 451 | Ok(tokens) 452 | } 453 | 454 | fn disallow_token(token: Option) -> syn::Result<()> { 455 | if let Some(token) = token { 456 | Err(syn::Error::new(token.span(), "Not allowed here")) 457 | } else { 458 | Ok(()) 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /tests/it/unimock.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_variables)] 3 | 4 | use std::future::Future; 5 | 6 | mod sync { 7 | use entrait::*; 8 | 9 | #[entrait(Foo)] 10 | fn foo(deps: &impl Bar) -> String { 11 | deps.bar() 12 | } 13 | 14 | #[entrait(Bar)] 15 | fn bar(_: &()) -> String { 16 | "string".to_string() 17 | } 18 | } 19 | 20 | trait Lol { 21 | fn hei(&self) -> impl Future + Send; 22 | } 23 | 24 | mod auth { 25 | use entrait::*; 26 | use unimock::*; 27 | 28 | type Error = (); 29 | 30 | #[derive(Clone)] 31 | pub struct User { 32 | username: String, 33 | hash: String, 34 | } 35 | 36 | #[entrait(GetUsername)] 37 | async fn get_username( 38 | rt: &impl Authenticate, 39 | id: u32, 40 | password: &str, 41 | ) -> Result { 42 | let user = rt.authenticate(id, password).await?; 43 | Ok(user.username) 44 | } 45 | 46 | #[entrait(Authenticate, mock_api=AuthenticateMock)] 47 | async fn authenticate( 48 | deps: &(impl FetchUser + VerifyPassword), 49 | id: u32, 50 | password: &str, 51 | ) -> Result { 52 | let user = deps.fetch_user(id).ok_or(())?; 53 | if deps.verify_password(password, &user.hash) { 54 | Ok(user) 55 | } else { 56 | Err(()) 57 | } 58 | } 59 | 60 | #[entrait(FetchUser, mock_api=FetchUserMock)] 61 | fn fetch_user(_: &T, _id: u32) -> Option { 62 | Some(User { 63 | username: "name".into(), 64 | hash: "h4sh".into(), 65 | }) 66 | } 67 | 68 | #[entrait(VerifyPassword, mock_api=VerifyPasswordMock)] 69 | fn verify_password(_: &T, _password: &str, _hash: &str) -> bool { 70 | true 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_get_username() { 75 | let username = get_username( 76 | &Unimock::new(AuthenticateMock.stub(|each| { 77 | each.call(matching!(_, _)).returns(Ok(User { 78 | username: "foobar".into(), 79 | hash: "h4sh".into(), 80 | })); 81 | })), 82 | 42, 83 | "pw", 84 | ) 85 | .await 86 | .unwrap(); 87 | assert_eq!("foobar", username); 88 | } 89 | 90 | #[tokio::test] 91 | async fn test_authenticate() { 92 | let mocks = Unimock::new(( 93 | FetchUserMock.each_call(matching!(42)).returns(Some(User { 94 | username: "foobar".into(), 95 | hash: "h4sh".into(), 96 | })), 97 | VerifyPasswordMock 98 | .each_call(matching!("pw", "h4sh")) 99 | .returns(true) 100 | .once(), 101 | )); 102 | 103 | let user = authenticate(&mocks, 42, "pw").await.unwrap(); 104 | assert_eq!("foobar", user.username); 105 | } 106 | 107 | #[tokio::test] 108 | async fn test_partial_no_overrides() { 109 | let user = authenticate(&Unimock::new_partial(()), 42, "pw") 110 | .await 111 | .unwrap(); 112 | 113 | assert_eq!("name", user.username); 114 | } 115 | 116 | #[tokio::test] 117 | async fn test_impl() { 118 | assert_eq!( 119 | "name", 120 | Impl::new(()).get_username(42, "password").await.unwrap() 121 | ); 122 | } 123 | } 124 | 125 | mod multi_mock { 126 | use entrait::*; 127 | use unimock::*; 128 | 129 | #[entrait(Bar, mock_api = BarMock)] 130 | async fn bar(_: &A) -> i32 { 131 | unimplemented!() 132 | } 133 | 134 | #[entrait(Baz, mock_api = BazMock)] 135 | async fn baz(_: &A) -> i32 { 136 | unimplemented!() 137 | } 138 | 139 | mod inline_bounds { 140 | use super::*; 141 | use entrait::entrait; 142 | 143 | #[entrait(Sum)] 144 | async fn sum(a: &A) -> i32 { 145 | a.bar().await + a.baz().await 146 | } 147 | 148 | #[tokio::test] 149 | async fn test_mock() { 150 | let mock = Unimock::new(( 151 | BarMock.each_call(matching!()).returns(40), 152 | BazMock.each_call(matching!()).returns(2), 153 | )); 154 | 155 | let result = sum(&mock).await; 156 | 157 | assert_eq!(42, result); 158 | } 159 | } 160 | 161 | mod where_bounds { 162 | use super::*; 163 | 164 | #[entrait(Sum)] 165 | async fn sum(a: &A) -> i32 166 | where 167 | A: Bar + Baz, 168 | { 169 | a.bar().await + a.baz().await 170 | } 171 | 172 | #[tokio::test] 173 | async fn test_mock() { 174 | assert_eq!( 175 | 42, 176 | sum(&Unimock::new(( 177 | BarMock.each_call(matching!()).returns(40), 178 | BazMock.each_call(matching!()).returns(2), 179 | ))) 180 | .await 181 | ); 182 | } 183 | } 184 | 185 | mod impl_trait_bounds { 186 | use super::*; 187 | 188 | #[entrait(Sum)] 189 | async fn sum(a: &(impl Bar + Baz)) -> i32 { 190 | a.bar().await + a.baz().await 191 | } 192 | 193 | #[tokio::test] 194 | async fn test_mock() { 195 | assert_eq!( 196 | 42, 197 | sum(&Unimock::new(( 198 | BarMock.each_call(matching!()).returns(40), 199 | BazMock.each_call(matching!()).returns(2), 200 | ))) 201 | .await 202 | ); 203 | } 204 | } 205 | } 206 | 207 | mod future_send_bound_for_tokio_spawn { 208 | use std::future::Future; 209 | 210 | use entrait::*; 211 | use unimock::*; 212 | 213 | #[entrait(Spawning)] 214 | async fn spawning(deps: &(impl Bar + Clone + Send + Sync + 'static)) -> i32 { 215 | let handles = [deps.clone(), deps.clone()] 216 | .into_iter() 217 | .map(|deps| tokio::spawn(async move { deps.bar().await })); 218 | 219 | let mut result = 0; 220 | 221 | for handle in handles { 222 | result += handle.await.unwrap(); 223 | } 224 | 225 | result 226 | } 227 | 228 | // important: takes T _by value_ 229 | #[entrait(Bar, mock_api = BarMock)] 230 | async fn bar(_: T) -> i32 { 231 | 1 232 | } 233 | 234 | #[tokio::test] 235 | async fn test_spawning_impl() { 236 | let result = spawning(&implementation::Impl::new(())).await; 237 | assert_eq!(2, result); 238 | } 239 | 240 | #[tokio::test] 241 | async fn test_spawning_partial_unmocked() { 242 | let result = spawning(&Unimock::new_partial(())).await; 243 | assert_eq!(2, result); 244 | } 245 | 246 | #[tokio::test] 247 | async fn test_spawning_override_bar() { 248 | let result = spawning(&Unimock::new_partial(( 249 | BarMock.next_call(matching!()).returns(1).once(), 250 | BarMock.next_call(matching!()).returns(2).once(), 251 | ))) 252 | .await; 253 | assert_eq!(3, result); 254 | } 255 | } 256 | 257 | mod more_async { 258 | use entrait::*; 259 | use unimock::*; 260 | struct State(u32); 261 | 262 | #[entrait(Foo)] 263 | async fn foo(a: &A) -> u32 { 264 | a.bar().await 265 | } 266 | 267 | #[entrait(Bar, mock_api = BarMock)] 268 | async fn bar(state: &State) -> u32 { 269 | state.0 270 | } 271 | 272 | #[tokio::test] 273 | async fn test() { 274 | let state = Impl::new(State(42)); 275 | let result = state.foo().await; 276 | 277 | assert_eq!(42, result); 278 | } 279 | 280 | #[tokio::test] 281 | async fn test_mock() { 282 | let result = foo(&Unimock::new( 283 | BarMock.each_call(matching!()).returns(84_u32), 284 | )) 285 | .await; 286 | 287 | assert_eq!(84, result); 288 | } 289 | 290 | #[tokio::test] 291 | async fn test_impl() { 292 | let state = Impl::new(State(42)); 293 | assert_eq!(42, state.foo().await); 294 | } 295 | } 296 | 297 | mod async_no_deps_etc { 298 | use entrait::*; 299 | use unimock::*; 300 | 301 | struct App; 302 | 303 | #[entrait(Foo, mock_api = FooMock)] 304 | async fn foo(deps: &impl Bar) -> i32 { 305 | deps.bar().await 306 | } 307 | 308 | #[entrait(Bar, mock_api = BarMock)] 309 | async fn bar(deps: &impl Baz) -> i32 { 310 | deps.baz().await 311 | } 312 | 313 | #[entrait(Baz, mock_api = BazMock)] 314 | async fn baz(_: &App) -> i32 { 315 | 42 316 | } 317 | 318 | #[entrait(NoDeps, no_deps)] 319 | async fn no_deps(arg: i32) -> i32 { 320 | arg 321 | } 322 | 323 | #[entrait(Borrow1)] 324 | async fn borrow1(_: &impl Bar) -> &i32 { 325 | panic!() 326 | } 327 | 328 | #[entrait(Borrow2)] 329 | async fn borrow2<'a, 'b>(_: &'a impl Bar, _arg: &'b i32) -> &'a i32 { 330 | panic!() 331 | } 332 | 333 | #[entrait(Borrow3)] 334 | async fn borrow3<'a>(_: &impl Bar, arg: &'a i32) -> &'a i32 { 335 | arg 336 | } 337 | 338 | #[allow(unused)] 339 | pub struct Borrowing<'a>(&'a i32); 340 | 341 | #[entrait(Borrow4)] 342 | async fn borrow4<'a>(_: &'a impl Bar, _arg: &i32) -> Borrowing<'a> { 343 | panic!() 344 | } 345 | 346 | #[tokio::test] 347 | async fn test_it() { 348 | let app = ::entrait::Impl::new(App); 349 | let _ = app.foo().await; 350 | } 351 | 352 | #[tokio::test] 353 | async fn mock_it() { 354 | let unimock = Unimock::new_partial(BazMock.each_call(matching!()).returns(42)); 355 | let answer = unimock.foo().await; 356 | 357 | assert_eq!(42, answer); 358 | } 359 | } 360 | 361 | mod generics { 362 | use entrait::*; 363 | use std::any::Any; 364 | use unimock::*; 365 | 366 | #[entrait(GenericDepsGenericReturn, mock_api = Mock1)] 367 | fn generic_deps_generic_return(_: &impl Any) -> T { 368 | Default::default() 369 | } 370 | 371 | #[entrait(ConcreteDepsGenericReturn, mock_api = Mock2)] 372 | fn concrete_deps_generic_return(_: &()) -> T { 373 | Default::default() 374 | } 375 | 376 | #[entrait(GenericDepsGenericParam, mock_api = Mock3)] 377 | fn generic_deps_generic_param(_: &impl Any, _arg: T) -> i32 { 378 | 42 379 | } 380 | 381 | #[entrait(ConcreteDepsGenericParam, mock_api = Mock4)] 382 | fn concrete_deps_generic_param(_: &(), _arg: T) -> i32 { 383 | 42 384 | } 385 | 386 | #[test] 387 | fn generic_mock_fns() { 388 | let s: String = Unimock::new( 389 | Mock1 390 | .with_types::() 391 | .some_call(matching!()) 392 | .returns("hey".to_string()), 393 | ) 394 | .generic_deps_generic_return(); 395 | 396 | assert_eq!("hey".to_string(), s); 397 | 398 | assert_eq!( 399 | 42, 400 | Unimock::new( 401 | Mock3 402 | .with_types::() 403 | .some_call(matching!("hey")) 404 | .returns(42), 405 | ) 406 | .generic_deps_generic_param("hey".to_string()) 407 | ); 408 | 409 | assert_eq!( 410 | 42, 411 | Unimock::new( 412 | Mock3 413 | .with_types::() 414 | .some_call(matching!(1337)) 415 | .returns(42), 416 | ) 417 | .generic_deps_generic_param(1337), 418 | ); 419 | } 420 | 421 | #[entrait(ConcreteDepsGenericReturnWhere)] 422 | fn concrete_deps_generic_return_where(_: &()) -> T 423 | where 424 | T: Default, 425 | { 426 | Default::default() 427 | } 428 | 429 | #[entrait(GenericDepsGenericReturnWhere)] 430 | fn generic_deps_generic_return_where(_: &impl Any) -> T 431 | where 432 | T: Default, 433 | { 434 | Default::default() 435 | } 436 | } 437 | 438 | mod destructuring_params { 439 | use entrait::entrait; 440 | 441 | pub struct NewType(T); 442 | 443 | #[entrait(Destructuring1)] 444 | fn destructuring1(_: &impl std::any::Any, NewType(num): NewType) {} 445 | 446 | #[entrait(Destructuring2)] 447 | fn destructuring2( 448 | _: &impl std::any::Any, 449 | NewType(num1): NewType, 450 | NewType(num2): NewType, 451 | ) { 452 | } 453 | 454 | #[entrait(DestructuringNoDeps, no_deps)] 455 | fn destructuring_no_deps(NewType(num1): NewType, NewType(num2): NewType) {} 456 | 457 | // Should become (arg1, _arg1) 458 | #[entrait(DestructuringNoDepsMixedConflict, no_deps)] 459 | fn destructuring_no_deps_mixed_confict(arg1: NewType, NewType(num2): NewType) {} 460 | 461 | // Should become (arg1, _arg1) 462 | #[entrait(WildcardParams, no_deps)] 463 | fn wildcard_params(_: i32, _: i32) {} 464 | } 465 | 466 | mod entrait_for_trait_unimock { 467 | use entrait::*; 468 | use unimock::*; 469 | 470 | #[entrait(mock_api=TraitMock)] 471 | trait Trait { 472 | fn method1(&self) -> i32; 473 | } 474 | 475 | #[test] 476 | fn entraited_trait_should_have_unimock_impl() { 477 | assert_eq!( 478 | 42, 479 | Unimock::new(TraitMock::method1.each_call(matching!()).returns(42)).method1() 480 | ); 481 | } 482 | 483 | #[test] 484 | #[should_panic( 485 | expected = "Trait::method1 cannot be unmocked as there is no function available to call." 486 | )] 487 | fn entraited_trait_should_not_be_unmockable() { 488 | Unimock::new_partial(()).method1(); 489 | } 490 | } 491 | 492 | mod naming_conflict_between_fn_and_param { 493 | use entrait::*; 494 | 495 | #[entrait(Foo)] 496 | fn foo(_: &T, foo: i32) {} 497 | } 498 | 499 | mod module { 500 | use entrait::*; 501 | use std::any::Any; 502 | use unimock::*; 503 | 504 | #[entrait(pub Foo, mock_api=FooMock)] 505 | fn foo(_: &impl Any) -> i32 { 506 | 7 507 | } 508 | 509 | #[entrait(pub BarBaz, mock_api=BarBazMock)] 510 | mod bar_baz { 511 | pub fn bar(deps: &impl super::Foo) -> i32 { 512 | deps.foo() 513 | } 514 | } 515 | 516 | fn takes_barbaz(deps: &impl BarBaz) -> i32 { 517 | deps.bar() 518 | } 519 | 520 | #[test] 521 | fn test_it() { 522 | let deps = Unimock::new(bar_baz::BarBazMock::bar.each_call(matching!()).returns(42)); 523 | assert_eq!(42, takes_barbaz(&deps)); 524 | } 525 | } 526 | 527 | mod module_async { 528 | use entrait::*; 529 | 530 | #[entrait(pub Mixed)] 531 | mod mixed { 532 | use std::any::Any; 533 | 534 | pub fn bar(_: &impl Any) {} 535 | pub async fn bar_async(_: &impl Any) {} 536 | } 537 | 538 | async fn takes_mixed(deps: &impl Mixed) { 539 | deps.bar(); 540 | let _ = deps.bar_async().await; 541 | } 542 | 543 | #[entrait(pub MultiAsync)] 544 | mod multi_async { 545 | use std::any::Any; 546 | 547 | pub async fn foo(_: &impl Any) {} 548 | pub async fn bar(_: &impl Any) {} 549 | } 550 | 551 | async fn takes_multi_async(deps: &impl MultiAsync) { 552 | deps.foo().await; 553 | deps.bar().await; 554 | } 555 | } 556 | 557 | mod module_generic { 558 | use entrait::*; 559 | 560 | #[entrait(M1)] 561 | mod m1 { 562 | use std::any::Any; 563 | 564 | pub fn a(_deps: &impl Any, arg: T) {} 565 | pub fn b(_deps: &impl Any, arg: U) {} 566 | } 567 | } 568 | 569 | #[test] 570 | fn level_without_mock_support() { 571 | use entrait::*; 572 | use unimock::*; 573 | 574 | #[entrait(A)] 575 | fn a(deps: &(impl B + C)) { 576 | deps.b(); 577 | deps.c(); 578 | } 579 | 580 | #[entrait(B, mock_api=BMock)] 581 | fn b(deps: &impl std::any::Any) {} 582 | 583 | #[entrait(CImpl, delegate_by = DelegateC)] 584 | pub trait C { 585 | fn c(&self); 586 | } 587 | 588 | #[entrait(pub D)] 589 | mod d {} 590 | 591 | fn takes_a(a: &impl A) {} 592 | fn takes_b(b: &impl B) {} 593 | fn takes_c(b: &impl C) {} 594 | 595 | takes_a(&Unimock::new(())); 596 | takes_b(&Unimock::new(())); 597 | takes_c(&Unimock::new(())); 598 | } 599 | 600 | // unimock issue https://github.com/audunhalland/unimock/issues/40 601 | mod arg_mutation_and_result_alias { 602 | use std::process::ExitStatus; 603 | 604 | use entrait::*; 605 | use unimock::*; 606 | 607 | type MyResult = Result; 608 | 609 | #[entrait(pub Exec, mock_api=ExecMock)] 610 | fn exec( 611 | _deps: &impl std::any::Any, 612 | command: &mut std::process::Command, 613 | ) -> MyResult<(ExitStatus, String)> { 614 | Err(()) 615 | } 616 | 617 | #[test] 618 | #[should_panic(expected = "Dead mocks should be removed")] 619 | fn test() { 620 | use std::os::unix::process::ExitStatusExt; 621 | 622 | Unimock::new( 623 | ExecMock 624 | .each_call(matching!((command) if command.get_program() == "editor")) 625 | .answers(&|_, _| Ok((ExitStatusExt::from_raw(1), String::new()))), 626 | ); 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /entrait_macros/src/entrait_trait/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementation for invoking entrait on a trait! 2 | 3 | pub mod input_attr; 4 | mod out_trait; 5 | 6 | use input_attr::EntraitTraitAttr; 7 | use proc_macro2::Span; 8 | 9 | use crate::analyze_generics::TraitFn; 10 | use crate::entrait_trait::input_attr::ImplTrait; 11 | use crate::generics; 12 | use crate::generics::TraitDependencyMode; 13 | use crate::idents::GenericIdents; 14 | use crate::input::FnInputMode; 15 | use crate::input::LiteralAttrs; 16 | use crate::opt::*; 17 | use crate::sub_attributes::analyze_sub_attributes; 18 | use crate::sub_attributes::SubAttribute; 19 | use crate::token_util::*; 20 | use crate::trait_codegen::Supertraits; 21 | use crate::trait_codegen::TraitCodegen; 22 | 23 | use proc_macro2::TokenStream; 24 | use quote::quote; 25 | use quote::ToTokens; 26 | 27 | use self::out_trait::OutTrait; 28 | 29 | #[derive(Clone, Copy)] 30 | struct ContainsAsync(bool); 31 | 32 | pub fn output_tokens( 33 | attr: EntraitTraitAttr, 34 | item_trait: syn::ItemTrait, 35 | ) -> syn::Result { 36 | if let (None, Some(SpanOpt(Delegate::ByTrait(_), span))) = 37 | (&attr.impl_trait, &attr.delegation_kind) 38 | { 39 | return Err(syn::Error::new( 40 | *span, 41 | "Cannot use a custom delegating trait without a custom trait to delegate to. Use either `#[entrait(TraitImpl, delegate_by = DelegateTrait)]` or `#[entrait(delegate_by = ref)]`", 42 | )); 43 | } 44 | 45 | let trait_ident_span = item_trait.ident.span(); 46 | let contains_async = ContainsAsync(item_trait.items.iter().any(|item| match item { 47 | syn::TraitItem::Fn(method) => method.sig.asyncness.is_some(), 48 | _ => false, 49 | })); 50 | 51 | let out_trait = out_trait::analyze_trait(item_trait)?; 52 | let sub_attributes = analyze_sub_attributes(&out_trait.attrs); 53 | let impl_sub_attributes: Vec<_> = sub_attributes 54 | .iter() 55 | .copied() 56 | .filter(|sub_attr| matches!(sub_attr, SubAttribute::AsyncTrait(_))) 57 | .collect(); 58 | 59 | let trait_dependency_mode = TraitDependencyMode::Generic(GenericIdents::new( 60 | &attr.crate_idents, 61 | out_trait.ident.span(), 62 | )); 63 | let generic_idents = match &trait_dependency_mode { 64 | TraitDependencyMode::Generic(idents) => idents, 65 | _ => panic!(), 66 | }; 67 | 68 | let delegation_trait_def = gen_impl_delegation_trait_defs( 69 | &out_trait, 70 | &trait_dependency_mode, 71 | generic_idents, 72 | &impl_sub_attributes, 73 | &attr, 74 | )?; 75 | 76 | let trait_def = TraitCodegen { 77 | crate_idents: &attr.crate_idents, 78 | opts: &attr.opts, 79 | trait_indirection: generics::TraitIndirection::Trait, 80 | trait_dependency_mode: &trait_dependency_mode, 81 | sub_attributes: &sub_attributes, 82 | } 83 | .gen_trait_def( 84 | &out_trait.vis, 85 | &out_trait.ident, 86 | &out_trait.generics, 87 | &out_trait.supertraits, 88 | &out_trait.fns, 89 | &FnInputMode::RawTrait(LiteralAttrs(&out_trait.attrs)), 90 | )?; 91 | 92 | let trait_ident = &out_trait.ident; 93 | let params = out_trait.generics.impl_params_from_idents( 94 | generic_idents, 95 | generics::TakesSelfByValue(false), // BUG? 96 | ); 97 | let args = out_trait 98 | .generics 99 | .arguments(&generics::ImplIndirection::None); 100 | let self_ty = generic_idents.impl_path(trait_ident_span); 101 | let where_clause = ImplWhereClause { 102 | out_trait: &out_trait, 103 | contains_async, 104 | trait_generics: &out_trait.generics, 105 | generic_idents, 106 | attr: &attr, 107 | span: trait_ident_span, 108 | }; 109 | 110 | let method_items = out_trait 111 | .fns 112 | .iter() 113 | .map(|trait_fn| gen_delegation_method(trait_fn, generic_idents, &attr, contains_async)); 114 | 115 | let out = quote! { 116 | #trait_def 117 | 118 | #delegation_trait_def 119 | 120 | #(#impl_sub_attributes)* 121 | impl #params #trait_ident #args for #self_ty #where_clause { 122 | #(#method_items)* 123 | } 124 | }; 125 | 126 | Ok(out) 127 | } 128 | 129 | fn gen_impl_delegation_trait_defs( 130 | out_trait: &OutTrait, 131 | trait_dependency_mode: &TraitDependencyMode, 132 | generic_idents: &GenericIdents, 133 | impl_sub_attributes: &[SubAttribute], 134 | attr: &EntraitTraitAttr, 135 | ) -> syn::Result> { 136 | let entrait = &generic_idents.crate_idents.entrait; 137 | 138 | let ImplTrait(_, impl_trait_ident) = match &attr.impl_trait { 139 | Some(impl_trait) => impl_trait, 140 | None => return Ok(None), 141 | }; 142 | 143 | let mut trait_copy = out_trait.clone(); 144 | trait_copy.ident = impl_trait_ident.clone(); 145 | 146 | let no_mock_opts = Opts { 147 | mock_api: None, 148 | unimock: None, 149 | mockall: None, 150 | ..attr.opts 151 | }; 152 | 153 | match &attr.delegation_kind { 154 | Some(SpanOpt(Delegate::ByTrait(delegation_ident), _)) => { 155 | trait_copy.generics.params.insert( 156 | 0, 157 | syn::parse_quote! { 158 | EntraitT 159 | }, 160 | ); 161 | for trait_fn in trait_copy.fns.iter_mut() { 162 | if !matches!(trait_fn.sig().inputs.first(), Some(syn::FnArg::Receiver(_))) { 163 | continue; 164 | } 165 | 166 | if let Some(first_arg) = trait_fn.entrait_sig.sig.inputs.first_mut() { 167 | if let syn::FnArg::Receiver(receiver) = first_arg { 168 | *first_arg = if let Some((and, lifetime)) = receiver.reference.clone() { 169 | syn::parse_quote! { 170 | __impl: #and #lifetime ::#entrait::Impl 171 | } 172 | } else { 173 | syn::parse_quote! { 174 | __impl: ::#entrait::Impl 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | let trait_def = TraitCodegen { 182 | crate_idents: &attr.crate_idents, 183 | opts: &no_mock_opts, 184 | trait_indirection: generics::TraitIndirection::StaticImpl, 185 | trait_dependency_mode, 186 | sub_attributes: impl_sub_attributes, 187 | } 188 | .gen_trait_def( 189 | &trait_copy.vis, 190 | &trait_copy.ident, 191 | &trait_copy.generics, 192 | &Supertraits::Some { 193 | colon_token: syn::token::Colon::default(), 194 | bounds: syn::parse_quote! { 'static }, 195 | }, 196 | &trait_copy.fns, 197 | &FnInputMode::RawTrait(LiteralAttrs(&[])), 198 | )?; 199 | 200 | Ok(Some(quote! { 201 | #(#impl_sub_attributes)* 202 | #trait_def 203 | 204 | pub trait #delegation_ident { 205 | type Target: #impl_trait_ident; 206 | } 207 | })) 208 | } 209 | Some(SpanOpt(Delegate::ByRef(_), _)) => { 210 | trait_copy.generics.params.insert( 211 | 0, 212 | syn::parse_quote! { 213 | EntraitT 214 | }, 215 | ); 216 | for trait_fn in trait_copy.fns.iter_mut() { 217 | if !matches!(trait_fn.sig().inputs.first(), Some(syn::FnArg::Receiver(_))) { 218 | continue; 219 | } 220 | 221 | trait_fn.entrait_sig.sig.inputs.insert( 222 | 1, 223 | syn::parse_quote! { 224 | __impl: &::#entrait::Impl 225 | }, 226 | ); 227 | } 228 | 229 | let no_mock_opts = Opts { 230 | mock_api: None, 231 | unimock: None, 232 | mockall: None, 233 | ..attr.opts 234 | }; 235 | 236 | let trait_def = TraitCodegen { 237 | crate_idents: &attr.crate_idents, 238 | opts: &no_mock_opts, 239 | trait_indirection: generics::TraitIndirection::DynamicImpl, 240 | trait_dependency_mode, 241 | sub_attributes: impl_sub_attributes, 242 | } 243 | .gen_trait_def( 244 | &trait_copy.vis, 245 | &trait_copy.ident, 246 | &trait_copy.generics, 247 | &Supertraits::Some { 248 | colon_token: syn::token::Colon::default(), 249 | bounds: syn::parse_quote! { 'static }, 250 | }, 251 | &trait_copy.fns, 252 | &FnInputMode::RawTrait(LiteralAttrs(&[])), 253 | )?; 254 | 255 | Ok(Some(quote! { 256 | #(#impl_sub_attributes)* 257 | #trait_def 258 | })) 259 | } 260 | _ => Err(syn::Error::new( 261 | proc_macro2::Span::call_site(), 262 | "Missing delegate_by", 263 | )), 264 | } 265 | } 266 | 267 | fn gen_delegation_method<'s>( 268 | trait_fn: &'s TraitFn, 269 | generic_idents: &'s GenericIdents, 270 | attr: &'s EntraitTraitAttr, 271 | contains_async: ContainsAsync, 272 | ) -> DelegatingMethod<'s> { 273 | let fn_sig = &trait_fn.sig(); 274 | let fn_ident = &fn_sig.ident; 275 | let impl_t = &generic_idents.impl_t; 276 | 277 | let arguments = fn_sig.inputs.iter().filter_map(|arg| match arg { 278 | syn::FnArg::Receiver(_) => None, 279 | syn::FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { 280 | syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_token_stream()), 281 | _ => panic!("Found a non-ident pattern, this should be handled in signature.rs"), 282 | }, 283 | }); 284 | let core = &generic_idents.crate_idents.core; 285 | 286 | match (&attr.impl_trait, &attr.delegation_kind) { 287 | (Some(ImplTrait(_, impl_trait_ident)), Some(SpanOpt(Delegate::ByTrait(_), _))) => { 288 | DelegatingMethod { 289 | trait_fn, 290 | call: quote! { 291 | // TODO: pass additional generic arguments(?) 292 | <#impl_t::Target as #impl_trait_ident<#impl_t>>::#fn_ident(self, #(#arguments),*) 293 | }, 294 | } 295 | } 296 | (Some(ImplTrait(_, impl_trait_ident)), Some(SpanOpt(Delegate::ByRef(ref_delegate), _))) => { 297 | let plus_sync = if contains_async.0 { 298 | Some(TokenPair( 299 | syn::token::Plus::default(), 300 | syn::Ident::new("Sync", Span::call_site()), 301 | )) 302 | } else { 303 | None 304 | }; 305 | let call = match ref_delegate { 306 | RefDelegate::AsRef => { 307 | quote! { 308 | <#impl_t as ::#core::convert::AsRef #plus_sync>>::as_ref(&*self) 309 | .#fn_ident(self, #(#arguments),*) 310 | } 311 | } 312 | RefDelegate::Borrow => { 313 | quote! { 314 | <#impl_t as ::#core::borrow::Borrow #plus_sync>>::borrow(&*self) 315 | .#fn_ident(self, #(#arguments),*) 316 | } 317 | } 318 | }; 319 | 320 | DelegatingMethod { trait_fn, call } 321 | } 322 | (None, Some(SpanOpt(Delegate::ByRef(RefDelegate::AsRef), _))) => DelegatingMethod { 323 | trait_fn, 324 | call: quote! { 325 | self.as_ref().as_ref().#fn_ident(#(#arguments),*) 326 | }, 327 | }, 328 | (None, Some(SpanOpt(Delegate::ByRef(RefDelegate::Borrow), _))) => DelegatingMethod { 329 | trait_fn, 330 | call: quote! { 331 | self.as_ref().borrow().#fn_ident(#(#arguments),*) 332 | }, 333 | }, 334 | _ => DelegatingMethod { 335 | trait_fn, 336 | call: quote! { 337 | self.as_ref().#fn_ident(#(#arguments),*) 338 | }, 339 | }, 340 | } 341 | } 342 | 343 | struct DelegatingMethod<'s> { 344 | trait_fn: &'s TraitFn, 345 | call: TokenStream, 346 | } 347 | 348 | impl ToTokens for DelegatingMethod<'_> { 349 | fn to_tokens(&self, stream: &mut TokenStream) { 350 | // Just "mirroring" all the attributes from 351 | // the trait definition to the implementation 352 | // is maybe a bit naive.. 353 | // There's a risk this will not always work in all cases. 354 | for attr in &self.trait_fn.attrs { 355 | push_tokens!(stream, attr); 356 | } 357 | 358 | self.trait_fn.sig().to_tokens(stream); 359 | syn::token::Brace::default().surround(stream, |stream| { 360 | // if self.needs_async_move && self.trait_fn.entrait_sig.associated_fut.is_some() { 361 | if false { 362 | push_tokens!( 363 | stream, 364 | syn::token::Async::default(), 365 | syn::token::Move::default() 366 | ); 367 | syn::token::Brace::default().surround(stream, |stream| { 368 | self.call.to_tokens(stream); 369 | push_tokens!( 370 | stream, 371 | syn::token::Dot::default(), 372 | syn::token::Await::default() 373 | ); 374 | }); 375 | } else if self.trait_fn.originally_async { 376 | self.call.to_tokens(stream); 377 | push_tokens!( 378 | stream, 379 | syn::token::Dot::default(), 380 | syn::token::Await::default() 381 | ); 382 | } else { 383 | self.call.to_tokens(stream); 384 | } 385 | }); 386 | } 387 | } 388 | 389 | struct ImplWhereClause<'g, 'c> { 390 | out_trait: &'g OutTrait, 391 | contains_async: ContainsAsync, 392 | trait_generics: &'g generics::TraitGenerics, 393 | generic_idents: &'g GenericIdents<'c>, 394 | attr: &'g EntraitTraitAttr, 395 | span: proc_macro2::Span, 396 | } 397 | 398 | impl ImplWhereClause<'_, '_> { 399 | fn push_impl_t_bounds(&self, stream: &mut TokenStream) { 400 | use syn::token::*; 401 | 402 | push_tokens!(stream, self.generic_idents.impl_t, Colon(self.span)); 403 | 404 | match (&self.attr.impl_trait, &self.attr.delegation_kind) { 405 | (Some(_), Some(SpanOpt(Delegate::ByTrait(delegate_ident), _))) => { 406 | push_tokens!( 407 | stream, 408 | delegate_ident, 409 | Lt(self.span), 410 | self.generic_idents.impl_t, 411 | Gt(self.span), 412 | self.plus_sync(), 413 | self.plus_static() 414 | ); 415 | } 416 | ( 417 | Some(ImplTrait(_, impl_trait_ident)), 418 | Some(SpanOpt(Delegate::ByRef(ref_delegate), _)), 419 | ) => { 420 | self.push_core_delegation_trait(stream, ref_delegate); 421 | push_tokens!( 422 | stream, 423 | // Generic arguments: 424 | Lt(self.span), 425 | Dyn(self.span), 426 | impl_trait_ident, 427 | Lt(self.span), 428 | self.generic_idents.impl_t, 429 | Gt(self.span), 430 | if self.contains_async.0 { 431 | Some(self.plus_sync()) 432 | } else { 433 | None 434 | }, 435 | Gt(self.span) 436 | ); 437 | 438 | if self.contains_async.0 { 439 | push_tokens!(stream, self.plus_send(), self.plus_sync()); 440 | } 441 | push_tokens!(stream, self.plus_static()); 442 | } 443 | (None, Some(SpanOpt(Delegate::ByRef(ref_delegate), _))) => { 444 | self.push_core_delegation_trait(stream, ref_delegate); 445 | push_tokens!( 446 | stream, 447 | Lt(self.span), 448 | Dyn(self.span), 449 | self.trait_with_arguments(), 450 | Gt(self.span) 451 | ); 452 | 453 | if self.contains_async.0 { 454 | push_tokens!(stream, self.plus_send(), self.plus_sync()); 455 | } 456 | push_tokens!(stream, self.plus_static()); 457 | } 458 | _delegate_to_impl_t => { 459 | push_tokens!(stream, self.trait_with_arguments(), self.plus_sync()); 460 | if self.contains_async.0 { 461 | // There will be a `self.as_ref().fn().await`, 462 | // that borrow will need to be 'static for the future to be Send 463 | push_tokens!(stream, self.plus_static()); 464 | } 465 | } 466 | } 467 | } 468 | 469 | fn push_core_delegation_trait(&self, stream: &mut TokenStream, ref_delegate: &RefDelegate) { 470 | use syn::token::*; 471 | match ref_delegate { 472 | RefDelegate::AsRef => { 473 | push_tokens!( 474 | stream, 475 | PathSep(self.span), 476 | self.generic_idents.crate_idents.core, 477 | PathSep(self.span), 478 | syn::Ident::new("convert", self.span), 479 | PathSep(self.span), 480 | syn::Ident::new("AsRef", self.span) 481 | ); 482 | } 483 | RefDelegate::Borrow => { 484 | push_tokens!( 485 | stream, 486 | PathSep(self.span), 487 | self.generic_idents.crate_idents.core, 488 | PathSep(self.span), 489 | syn::Ident::new("borrow", self.span), 490 | PathSep(self.span), 491 | syn::Ident::new("Borrow", self.span) 492 | ); 493 | } 494 | } 495 | } 496 | 497 | fn trait_with_arguments(&self) -> TokenPair { 498 | TokenPair( 499 | &self.out_trait.ident, 500 | self.trait_generics 501 | .arguments(&generics::ImplIndirection::None), 502 | ) 503 | } 504 | 505 | fn plus_static(&self) -> TokenPair { 506 | TokenPair( 507 | syn::token::Plus(self.span), 508 | syn::Lifetime::new("'static", self.span), 509 | ) 510 | } 511 | 512 | fn plus_send(&self) -> TokenPair { 513 | TokenPair( 514 | syn::token::Plus(self.span), 515 | syn::Ident::new("Send", self.span), 516 | ) 517 | } 518 | 519 | fn plus_sync(&self) -> TokenPair { 520 | TokenPair( 521 | syn::token::Plus(self.span), 522 | syn::Ident::new("Sync", self.span), 523 | ) 524 | } 525 | } 526 | 527 | impl quote::ToTokens for ImplWhereClause<'_, '_> { 528 | fn to_tokens(&self, stream: &mut TokenStream) { 529 | let mut punctuator = Punctuator::new( 530 | stream, 531 | syn::token::Where(self.span), 532 | syn::token::Comma(self.span), 533 | EmptyToken, 534 | ); 535 | 536 | // Bounds on the `T` in `Impl`: 537 | punctuator.push_fn(|stream| { 538 | self.push_impl_t_bounds(stream); 539 | }); 540 | 541 | for predicate in &self.trait_generics.where_predicates { 542 | punctuator.push(predicate); 543 | } 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # entrait 2 | 3 | [crates.io](https://crates.io/crates/entrait) 4 | [docs.rs](https://docs.rs/entrait) 5 | [CI](https://github.com/audunhalland/entrait/actions?query=branch%3Amain) 6 | [Discord](https://discord.gg/nw9ZWP9kvy) 7 | 8 | 9 | 10 | A proc macro for designing loosely coupled Rust applications. 11 | 12 | [`entrait`](entrait) is used to generate an _implemented trait_ from the definition of regular functions. 13 | The emergent pattern that results from its use enable the following things: 14 | * Zero-cost loose coupling and inversion of control 15 | * Dependency graph as a compile time concept 16 | * Mock library integrations 17 | * Clean, readable, boilerplate-free code 18 | 19 | The resulting pattern is referred to as [the entrait pattern](https://audunhalland.github.io/blog/entrait-pattern/) (see also: [philosophy](#philosophy)). 20 | 21 | ## Introduction 22 | 23 | The macro looks like this: 24 | 25 | ```rust 26 | #[entrait(MyFunction)] 27 | fn my_function(deps: &D) { 28 | } 29 | ``` 30 | 31 | which generates a new single-method trait named `MyFunction`, with the method signature derived from the original function. 32 | Entrait is a pure append-only macro: It will never alter the syntax of your function. 33 | The new language items it generates will appear below the function. 34 | 35 | In the first example, `my_function` has a single parameter called `deps` which is generic over a type `D`, and represents dependencies injected into the function. 36 | The dependency parameter is always the first parameter, which is analogous to the `&self` parameter of the generated trait method. 37 | 38 | To add a dependency, we just introduce a trait bound, now expressable as `impl Trait`. 39 | This is demonstrated by looking at one function calling another: 40 | 41 | ```rust 42 | #[entrait(Foo)] 43 | fn foo(deps: &impl Bar) { 44 | println!("{}", deps.bar(42)); 45 | } 46 | 47 | #[entrait(Bar)] 48 | fn bar(deps: &D, n: i32) -> String { 49 | format!("You passed {n}") 50 | } 51 | ``` 52 | 53 | 54 | #### Multiple dependencies 55 | Other frameworks might represent multiple dependencies by having one value for each one, but entrait represents all dependencies _within the same value_. 56 | When the dependency parameter is generic, its trait bounds specifiy what methods we expect to be callable inside the function. 57 | 58 | Multiple bounds can be expressed using the `&(impl A + B)` syntax. 59 | 60 | The single-value dependency design means that it is always the same reference that is passed around everywhere. 61 | But a reference to what, exactly? 62 | This is what we have managed to abstract away, which is the [whole point](#testing). 63 | 64 | 65 | 66 | #### Runtime and implementation 67 | When we want to compile a working application, we need an actual type to inject into the various entrait entrypoints. 68 | Two things will be important: 69 | 70 | * All trait bounds used deeper in the graph will implicitly "bubble up" to the entrypoint level, so the type we eventually use will need to implement all those traits in order to type check. 71 | * The implementations of these traits need to do the correct thing: Actually call the entraited function, so that the dependency graph is turned into an actual _call graph_. 72 | 73 | Entrait generates _implemented traits_, and the type to use for linking it all together is `Impl`: 74 | 75 | ```rust 76 | #[entrait(Foo)] 77 | fn foo(deps: &impl Bar) -> i32 { 78 | deps.bar() 79 | } 80 | 81 | #[entrait(Bar)] 82 | fn bar(_deps: &impl std::any::Any) -> i32 { 83 | 42 84 | } 85 | 86 | let app = Impl::new(()); 87 | assert_eq!(42, app.foo()); 88 | ``` 89 | 90 |

91 | 🔬 Inspect the generated code 🔬 92 | 93 | The linking happens in the generated impl block for `Impl`, putting the entire impl under a where clause derived from the original dependency bounds: 94 | 95 | ```rust 96 | impl Foo for Impl where Self: Bar { 97 | fn foo(&self) -> i32 { 98 | foo(self) // <---- calls your function 99 | } 100 | } 101 | ``` 102 |
103 | 104 | `Impl` is generic, so we can put whatever type we want into it. 105 | Normally this would be some type that represents the global state/configuration of the running application. 106 | But if dependencies can only be traits, and we always abstract away this type, how can this state ever be accessed? 107 | 108 | 109 | 110 | #### Concrete dependencies 111 | So far we have only seen generic trait-based dependencies, but the dependency can also be a _concrete type_: 112 | 113 | ```rust 114 | struct Config(i32); 115 | 116 | #[entrait(UseTheConfig)] 117 | fn use_the_config(config: &Config) -> i32 { 118 | config.0 119 | } 120 | 121 | #[entrait(DoubleIt)] 122 | fn double_it(deps: &impl UseTheConfig) -> i32 { 123 | deps.use_the_config() * 2 124 | } 125 | 126 | assert_eq!(42, Impl::new(Config(21)).double_it()); 127 | ``` 128 | 129 | The parameter of `use_the_config` is in the first position, so it represents the dependency. 130 | 131 | We will notice two interesting things: 132 | * Functions that depend on `UseTheConfig`, either directly or indirectly, now have only one valid dependency type: `Impl`[1](#case-1-concrete-leaf-dependencies). 133 | * Inside `use_the_config`, we have a `&Config` reference instead of `&Impl`. This means we cannot call other entraited functions, because they are not implemented for `Config`. 134 | 135 | The last point means that a concrete dependency is the end of the line, a leaf in the dependency graph. 136 | 137 | Typically, functions with a concrete dependency should be kept small and avoid extensive business logic. 138 | They ideally function as accessors, providing a loosely coupled abstraction layer over concrete application state. 139 | 140 | 141 | #### Module support 142 | To reduce the number of generated traits, entrait can be used as a `mod` attribute. 143 | When used in this mode, the macro will look for non-private functions directly within the module scope, to be represented as methods on the resulting trait. 144 | This mode works mostly identically to the standalone function mode. 145 | 146 | ```rust 147 | #[entrait(pub MyModule)] 148 | mod my_module { 149 | pub fn foo(deps: &impl super::SomeTrait) {} 150 | pub fn bar(deps: &impl super::OtherTrait) {} 151 | } 152 | ``` 153 | This example generates a `MyModule` trait containing the methods `foo` and `bar`. 154 | 155 | 156 | ## Testing 157 | ### Trait mocking with `Unimock` 158 | 159 | The whole point of entrait is to provide inversion of control, so that alternative dependency implementations can be used when unit testing function bodies. 160 | While test code can contain manual trait implementations, the most ergonomic way to test is to use a mocking library, which provides more features with less code. 161 | 162 | Entrait works best together with [unimock](https://docs.rs/unimock/latest/unimock/), as these two crates have been designed from the start with each other in mind. 163 | 164 | Unimock exports a single mock struct which can be passed as argument to every function that accept a generic `deps` parameter 165 | (given that entrait is used with unimock support everywhere). 166 | To enable mock configuration of entraited functions, supply the `mock_api` option, e.g. `mock_api=TraitMock` if the name of the trait is `Trait`. 167 | This works the same way for entraited modules, only that those already _have_ a module to export from. 168 | 169 | Unimock support for entrait is enabled by passing the `unimock` option to entrait (`#[entrait(Foo, unimock)]`), or turning on the `unimock` _feature_, which makes all entraited functions mockable, even in upstream crates (as long as `mock_api` is provided.). 170 | 171 | ```rust 172 | #[entrait(Foo, mock_api=FooMock)] 173 | fn foo(_: &D) -> i32 { 174 | unimplemented!() 175 | } 176 | #[entrait(MyMod, mock_api=mock)] 177 | mod my_mod { 178 | pub fn bar(_: &D) -> i32 { 179 | unimplemented!() 180 | } 181 | } 182 | 183 | fn my_func(deps: &(impl Foo + MyMod)) -> i32 { 184 | deps.foo() + deps.bar() 185 | } 186 | 187 | let mocked_deps = Unimock::new(( 188 | FooMock.each_call(matching!()).returns(40), 189 | my_mod::mock::bar.each_call(matching!()).returns(2), 190 | )); 191 | 192 | assert_eq!(42, my_func(&mocked_deps)); 193 | ``` 194 | 195 | ##### Deep integration testing with unimock 196 | Entrait with unimock supports _un-mocking_. This means that the test environment can be _partially mocked!_ 197 | 198 | ```rust 199 | #[entrait(SayHello)] 200 | fn say_hello(deps: &impl FetchPlanetName, planet_id: u32) -> Result { 201 | Ok(format!("Hello {}!", deps.fetch_planet_name(planet_id)?)) 202 | } 203 | 204 | #[entrait(FetchPlanetName)] 205 | fn fetch_planet_name(deps: &impl FetchPlanet, planet_id: u32) -> Result { 206 | let planet = deps.fetch_planet(planet_id)?; 207 | Ok(planet.name) 208 | } 209 | 210 | pub struct Planet { 211 | name: String 212 | } 213 | 214 | #[entrait(FetchPlanet, mock_api=FetchPlanetMock)] 215 | fn fetch_planet(deps: &(), planet_id: u32) -> Result { 216 | unimplemented!("This doc test has no access to a database :(") 217 | } 218 | 219 | let hello_string = say_hello( 220 | &Unimock::new_partial( 221 | FetchPlanetMock 222 | .some_call(matching!(123456)) 223 | .returns(Ok(Planet { 224 | name: "World".to_string(), 225 | })) 226 | ), 227 | 123456, 228 | ).unwrap(); 229 | 230 | assert_eq!("Hello World!", hello_string); 231 | ``` 232 | 233 | This example used [`Unimock::new_partial`](unimock::Unimock::new_partial) to create a mocker that works mostly like `Impl`, except that the call graph can be short-circuited at arbitrary, run-time configurable points. 234 | The example code goes through three layers (`say_hello => fetch_planet_name => fetch_planet`), and only the deepest one gets mocked out. 235 | 236 | 237 | #### Alternative mocking: Mockall 238 | If you instead wish to use a more established mocking crate, there is also support for [mockall](https://docs.rs/mockall/latest/mockall/). 239 | Note that mockall has some limitations. 240 | Multiple trait bounds are not supported, and deep tests will not work. 241 | Also, mockall tends to generate a lot of code, often an order of magnitude more than unimock. 242 | 243 | Enabling mockall is done using the `mockall` entrait option. 244 | There is no cargo feature to turn this on implicitly, because mockall doesn't work well when it's re-exported through another crate. 245 | 246 | ```rust 247 | #[entrait(Foo, mockall)] 248 | fn foo(_: &D) -> u32 { 249 | unimplemented!() 250 | } 251 | 252 | fn my_func(deps: &impl Foo) -> u32 { 253 | deps.foo() 254 | } 255 | 256 | fn main() { 257 | let mut deps = MockFoo::new(); 258 | deps.expect_foo().returning(|| 42); 259 | assert_eq!(42, my_func(&deps)); 260 | } 261 | ``` 262 | 263 | 264 | ## Multi-crate architecture 265 | 266 | A common technique for Rust application development is to choose a multi-crate architecture. 267 | There are usually two main ways to go about it: 268 | 269 | 1. The call graph and crate dependency go in the same direction. 270 | 2. The call graph and crate dependency go in _opposite_ directions. 271 | 272 | The first option is how libraries are normally used: Its functions are just called, without any indirection. 273 | 274 | The second option can be referred to as a variant of the 275 | [dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle). 276 | This is usually a desirable architectural property, and achieving this with entrait is what this section is about. 277 | 278 | The main goal is to be able to express business logic _centrally_, and avoid depending directly on infrastructure details (onion architecture). 279 | All of the examples in this section make some use of traits and trait delegation. 280 | 281 | 282 | #### Case 1: Concrete leaf dependencies 283 | Earlier it was mentioned that when concrete-type dependencies are used, the `T` in `Impl`, your application, and the type of the dependency have to match. 284 | But this is only partially true. 285 | It really comes down to which traits are implemented on what types: 286 | 287 | ```rust 288 | pub struct Config { 289 | foo: String, 290 | } 291 | 292 | #[entrait_export(pub GetFoo)] 293 | fn get_foo(config: &Config) -> &str { 294 | &config.foo 295 | } 296 | ``` 297 | 298 |
299 | 🔬 Inspect the generated code 🔬 300 | 301 | ```rust 302 | trait GetFoo { 303 | fn get_foo(&self) -> &str; 304 | } 305 | impl GetFoo for Impl { 306 | fn get_foo(&self) -> &str { 307 | self.as_ref().get_foo() 308 | } 309 | } 310 | impl GetFoo for Config { 311 | fn get_foo(&self) -> &str { 312 | get_foo(self) 313 | } 314 | } 315 | ``` 316 | 317 |
318 | 319 | Here we actually have a trait `GetFoo` that is implemented two times: for `Impl where T: GetFoo` and for `Config`. 320 | The first implementation is delegating to the other one. 321 | 322 | For making this work with _any_ downstream application type, we just have to manually implement `GetFoo` for that application: 323 | 324 | ```rust 325 | struct App { 326 | config: some_upstream_crate::Config, 327 | } 328 | impl some_upstream_crate::GetFoo for App { 329 | fn get_foo(&self) -> &str { 330 | self.config.get_foo() 331 | } 332 | } 333 | ``` 334 | 335 | 336 | #### Case 2: Hand-written trait as a leaf dependency 337 | Using a concrete type like `Config` from the first case can be contrived in many situations. 338 | Sometimes a good old hand-written trait definition will do the job much better: 339 | 340 | ```rust 341 | #[entrait] 342 | pub trait System { 343 | fn current_time(&self) -> u128; 344 | } 345 | ``` 346 | 347 |
348 | 🔬 Inspect the generated code 🔬 349 | 350 | ```rust 351 | impl System for Impl { 352 | fn current_time(&self) -> u128 { 353 | self.as_ref().current_time() 354 | } 355 | } 356 | ``` 357 | 358 |
359 | 360 | What the attribute does in this case, is just to generate the correct blanket implementations of the trait: _delegation_ and _mocks_. 361 | 362 | To use with some `App`, the app type itself should implement the trait. 363 | 364 | 365 | #### Case 3: Hand-written trait as a leaf dependency using _dynamic dispatch_ 366 | Sometimes it might be desirable to have a delegation that involves dynamic dispatch. 367 | Entrait has a `delegate_by =` option, where you can pass an alternative trait to use as part of the delegation strategy. 368 | To enable dynamic dispatch, use [`ref`](https://doc.rust-lang.org/stable/core/convert/trait.AsRef.html): 369 | 370 | ```rust 371 | #[entrait(delegate_by=ref)] 372 | trait ReadConfig: 'static { 373 | fn read_config(&self) -> &str; 374 | } 375 | ``` 376 | 377 |
378 | 🔬 Inspect the generated code 🔬 379 | 380 | ```rust 381 | impl + 'static> ReadConfig for Impl { 382 | fn read_config(&self) -> &str { 383 | self.as_ref().as_ref().read_config() 384 | } 385 | } 386 | ``` 387 | 388 |
389 | 390 | To use this together with some `App`, it should implement the [`AsRef`](https://doc.rust-lang.org/stable/core/convert/trait.AsRef.html) trait. 391 | 392 | 393 | #### Case 4: Truly inverted _internal dependencies_ - static dispatch 394 | All cases up to this point have been _leaf dependencies_. 395 | Leaf dependencies are delegations that exit from the `Impl` layer, using delegation targets involving concete `T`'s. 396 | This means that it is impossible to continue to use the entrait pattern and extend your application behind those abstractions. 397 | 398 | To make your abstraction _extendable_ and your dependency _internal_, we have to keep the `T` generic inside the [Impl] type. 399 | To make this work, we have to make use of two helper traits: 400 | 401 | ```rust 402 | #[entrait(RepositoryImpl, delegate_by = DelegateRepository)] 403 | pub trait Repository { 404 | fn fetch(&self) -> i32; 405 | } 406 | ``` 407 | 408 |
409 | 🔬 Inspect the generated code 🔬 410 | 411 | ```rust 412 | pub trait RepositoryImpl { 413 | fn fetch(_impl: &Impl) -> i32; 414 | } 415 | pub trait DelegateRepository { 416 | type Target: RepositoryImpl; 417 | } 418 | impl> Repository for Impl { 419 | fn fetch(&self) -> i32 { 420 | >::Target::fetch(self) 421 | } 422 | } 423 | ``` 424 | 425 |
426 | 427 | This syntax introduces a total of _three_ traits: 428 | 429 | * `Repository`: The _dependency_, what the rest of the application directly calls. 430 | * `RepositoryImpl`: The _delegation target_, a trait which needs to be implemented by some `Target` type. 431 | * `DelegateRepository`: The _delegation selector_, that selects the specific `Target` type to be used for some specific `App`. 432 | 433 | This design makes it possible to separate concerns into three different crates, ordered from most-upstream to most-downstream: 434 | 1. _Core logic:_ Depend on and call `Repository` methods. 435 | 2. _External system integration:_ Provide some implementation of the repository, by implementing `RepositoryImpl`. 436 | 3. _Executable:_ Construct an `App` that selects a specific repository implementation from crate 2. 437 | 438 | All delegation from `Repository` to `RepositoryImpl` goes via the `DelegateRepository` trait. 439 | The method signatures in `RepositoryImpl` are _static_, and receives the `&Impl` via a normal parameter. 440 | This allows us to continue using entrait patterns within those implementations! 441 | 442 | In _crate 2_, we have to provide an implementation of `RepositoryImpl`. 443 | This can either be done manually, or by using the [entrait] attribute on an `impl` block: 444 | 445 | ```rust 446 | pub struct MyRepository; 447 | 448 | #[entrait] 449 | impl crate1::RepositoryImpl for MyRepository { 450 | // this function has the now-familiar entrait-compatible signature: 451 | fn fetch(deps: &D) -> i32 { 452 | unimplemented!() 453 | } 454 | } 455 | ``` 456 | 457 |
458 | 🔬 Inspect the generated code 🔬 459 | 460 | ```rust 461 | impl MyRepository { 462 | fn fetch(deps: &D) -> i32 { 463 | unimplemented!() 464 | } 465 | } 466 | impl crate1::RepositoryImpl for MyRepository { 467 | #[inline] 468 | fn fetch(_impl: &Impl) -> i32 { 469 | Self::fetch(_impl) 470 | } 471 | } 472 | ``` 473 | 474 |
475 | 476 | Entrait will split this trait implementation block in two: An _inherent_ one containing the original code, and a proper trait implementation which performs the delegation. 477 | 478 | In the end, we just have to implement our `DelegateRepository`: 479 | 480 | ```rust 481 | // in crate3: 482 | struct App; 483 | impl crate1::DelegateRepository for App { 484 | type Target = crate2::MyRepository; 485 | } 486 | fn main() { /* ... */ } 487 | ``` 488 | 489 | 490 | #### Case 5: Truly inverted internal dependencies - dynamic dispatch 491 | A small variation of case 4: Use `delegate_by=ref` instead of a custom trait. 492 | This makes the delegation happen using dynamic dispatch. 493 | 494 | The implementation syntax is almost the same as in case 4, only that the entrait attribute must now be `#[entrait(ref)]`: 495 | 496 | ```rust 497 | #[entrait(RepositoryImpl, delegate_by=ref)] 498 | pub trait Repository { 499 | fn fetch(&self) -> i32; 500 | } 501 | 502 | pub struct MyRepository; 503 | 504 | #[entrait(ref)] 505 | impl RepositoryImpl for MyRepository { 506 | fn fetch(deps: &D) -> i32 { 507 | unimplemented!() 508 | } 509 | } 510 | ``` 511 | 512 | The app must now implement [`AsRef>`](https://doc.rust-lang.org/stable/core/convert/trait.AsRef.html). 513 | 514 | 515 | 516 | 517 | ## Options and features 518 | 519 | ##### Trait visibility 520 | by default, entrait generates a trait that is module-private (no visibility keyword). 521 | To change this, just put a visibility specifier before the trait name: 522 | 523 | ```rust 524 | use entrait::*; 525 | #[entrait(pub Foo)] // <-- public trait 526 | fn foo(deps: &D) { // <-- private function 527 | } 528 | ``` 529 | 530 | ##### async support 531 | Zero-cost, static-dispatch `async` works out of the box[^1]. 532 | 533 | When dynamic dispatch is needed, for example in combination with `delegate_by=ref`, entrait understands the `#[async_trait]` attribute when applied _after_ the entrait macro. 534 | Entrait will re-apply that macro to the various generated impl blocks as needed. 535 | 536 | ###### async `Send`-ness 537 | Similar to `async_trait`, entrait generates a [Send]-bound on futures by default. 538 | To opt out of the Send bound, pass `?Send` as a macro argument: 539 | 540 | ```rust 541 | #[entrait(ReturnRc, ?Send)] 542 | async fn return_rc(_deps: impl Any) -> Rc { 543 | Rc::new(42) 544 | } 545 | ``` 546 | 547 | ##### Integrating with other `fn`-targeting macros, and `no_deps` 548 | Some macros are used to transform the body of a function, or generate a body from scratch. 549 | For example, we can use [`feignhttp`](https://docs.rs/feignhttp/latest/feignhttp/) to generate an HTTP client. Entrait will try as best as it 550 | can to co-exist with macros like these. Since `entrait` is a higher-level macro that does not touch fn bodies (it does not even try to parse them), 551 | entrait should be processed after, which means it should be placed _before_ lower level macros. Example: 552 | 553 | ```rust 554 | #[entrait(FetchThing, no_deps)] 555 | #[feignhttp::get("https://my.api.org/api/{param}")] 556 | async fn fetch_thing(#[path] param: String) -> feignhttp::Result {} 557 | ``` 558 | 559 | Here we had to use the `no_deps` entrait option. 560 | This is used to tell entrait that the function does not have a `deps` parameter as its first input. 561 | Instead, all the function's inputs get promoted to the generated trait method. 562 | 563 | ##### Conditional compilation of mocks 564 | Most often, you will only need to generate mock implementations for test code, and skip this for production code. 565 | A notable exception to this is when building libraries. 566 | When an application consists of several crates, downstream crates would likely want to mock out functionality from libraries. 567 | 568 | Entrait calls this _exporting_, and it unconditionally turns on autogeneration of mock implementations: 569 | 570 | ```rust 571 | #[entrait_export(pub Bar)] 572 | fn bar(deps: &()) {} 573 | ``` 574 | or 575 | ```rust 576 | #[entrait(pub Foo, export)] 577 | fn foo(deps: &()) {} 578 | ``` 579 | 580 | It is also possible to reduce noise by doing `use entrait::entrait_export as entrait`. 581 | 582 | ##### Feature overview 583 | | Feature | Implies | Description | 584 | | ------------------- | --------------- | ------------------- | 585 | | `unimock` | | Adds the [unimock] dependency, and turns on Unimock implementations for all traits. | 586 | 587 | 588 | ## "Philosophy" 589 | The `entrait` crate is central to the _entrait pattern_, an opinionated yet flexible and _Rusty_ way to build testable applications/business logic. 590 | 591 | To understand the entrait model and how to achieve Dependency Injection (DI) with it, we can compare it with a more widely used and classical alternative pattern: 592 | _Object-Oriented DI_. 593 | 594 | In object-oriented DI, each named dependency is a separate object instance. 595 | Each dependency exports a set of public methods, and internally points to a set of private dependencies. 596 | A working application is built by fully instantiating such an _object graph_ of interconnected dependencies. 597 | 598 | Entrait was built to address two drawbacks inherent to this design: 599 | 600 | * Representing a _graph_ of objects (even if acyclic) in Rust usually requires reference counting/heap allocation. 601 | * Each "dependency" abstraction often contains a lot of different functionality. 602 | As an example, consider [DDD](https://en.wikipedia.org/wiki/Domain-driven_design)-based applications consisting of `DomainServices`. 603 | There will typically be one such class per domain object, with a lot of methods in each. 604 | This results in dependency graphs with fewer nodes overall, but the number of possible _call graphs_ is much larger. 605 | A common problem with this is that the _actual dependencies_—the functions actually getting called—are encapsulated 606 | and hidden away from public interfaces. 607 | To construct valid dependency mocks in unit tests, a developer will have to read through full function bodies instead of looking at signatures. 608 | 609 | `entrait` solves this by: 610 | 611 | * Representing dependencies as _traits_ instead of types, automatically profiting from Rust's builtin zero-cost abstraction tool. 612 | * Giving users a choice between fine and coarse dependency granularity, by enabling both single-function traits and module-based traits. 613 | * Always declaring dependencies at the function signature level, close to call sites, instead of at module level. 614 | 615 | 616 | ## Limitations 617 | This section lists known limitations of entrait: 618 | 619 | #### Cyclic dependency graphs 620 | Cyclic dependency graphs are impossible with entrait. 621 | In fact, this is not a limit of entrait itself, but with Rust's trait solver. 622 | It is not able to prove that a type implements a trait if it needs to prove that it does in order to prove it. 623 | 624 | While this is a limitation, it is not necessarily a bad one. 625 | One might say that a layered application architecture should never contain cycles. 626 | If you do need recursive algorithms, you could model this as utility functions outside of the entraited APIs of the application. 627 | 628 | [^1]: Literally, out of the [Box]! In entrait version 0.7 and newer, asynchronous functions are zero-cost by default. 629 | 630 | 631 | --------------------------------------------------------------------------------