Skip to main content

bitwarden_core_macro/
lib.rs

1//! Proc macros for the Bitwarden SDK.
2//!
3//! Provides:
4//! - `#[derive(FromClient)]` derive macro for implementing the `FromClient` trait on client
5//!   structs.
6//! - `#[client_trait]` attribute macro that generates a `FromClientShared` bridge for a feature
7//!   client's hand-written trait.
8
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{DeriveInput, Expr, ItemTrait, parse::Parser, parse_macro_input};
12
13/// Derive macro for implementing the `FromClient` trait on client structs.
14///
15/// This macro generates an implementation of the `FromClient` trait that extracts
16/// all struct fields from a `Client` using the `FromClientPart` trait.
17///
18/// # Example
19///
20/// ```ignore
21/// use bitwarden_core::client::FromClient;
22/// use bitwarden_core_macro::FromClient;
23///
24/// #[derive(FromClient)]
25/// pub struct FoldersClient {
26///     key_store: KeyStore<KeySlotIds>,
27///     api_configurations: Arc<ApiConfigurations>,
28///     repository: Option<Arc<dyn Repository<Folder>>>,
29/// }
30/// ```
31///
32/// The macro generates:
33///
34/// ```ignore
35/// impl FromClient for FoldersClient {
36///     fn from_client(client: &Client) -> Self {
37///         Self {
38///             key_store: FromClientPart::<KeyStore<KeySlotIds>>::get_part(client),
39///             api_configs: FromClientPart::<Arc<ApiConfigurations>>::get_part(client),
40///             repository: FromClientPart::<Option<Arc<dyn Repository<Folder>>>>::get_part(client),
41///         }
42///     }
43/// }
44/// ```
45#[proc_macro_derive(FromClient)]
46pub fn derive_from_client(item: TokenStream) -> TokenStream {
47    let input = parse_macro_input!(item as DeriveInput);
48
49    let struct_name = &input.ident;
50    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
51
52    let syn::Data::Struct(syn::DataStruct {
53        fields: syn::Fields::Named(fields),
54        ..
55    }) = &input.data
56    else {
57        return syn::Error::new_spanned(
58            &input,
59            "FromClient can only be derived for structs with named fields",
60        )
61        .to_compile_error()
62        .into();
63    };
64
65    let field_inits = fields.named.iter().filter_map(|f| {
66        let field_name = f.ident.as_ref()?;
67        let field_type = &f.ty;
68        Some(quote! {
69            #field_name: ::bitwarden_core::client::FromClientPart::<#field_type>::get_part(client)
70        })
71    });
72
73    let expanded = quote! {
74        impl #impl_generics ::bitwarden_core::client::FromClient for #struct_name #ty_generics #where_clause {
75            fn from_client(client: &::bitwarden_core::Client) -> Self {
76                Self {
77                    #(#field_inits),*
78                }
79            }
80        }
81    };
82
83    TokenStream::from(expanded)
84}
85
86/// Attribute macro that emits the `FromClientShared` bridge for a hand-written feature client
87/// trait.
88///
89/// Applied to a `trait FooTrait { ... }` declaration with a `via = <expression>` argument, it
90/// re-emits the trait unchanged and adds:
91///
92/// ```ignore
93/// impl FromClientShared for dyn FooTrait {
94///     fn from_client_shared(client: &Client) -> Arc<Self> {
95///         Arc::new(<expression>)
96///     }
97/// }
98/// ```
99///
100/// The expression has `client: &Client` in scope and must evaluate to a value that can be
101/// wrapped in `Arc<dyn FooTrait>` (typically a concrete struct that implements the trait).
102///
103/// # Example
104///
105/// ```ignore
106/// #[client_trait(via = client.folders())]
107/// #[cfg_attr(any(test, feature = "test-fixtures"), mockall::automock)]
108/// #[async_trait::async_trait]
109/// pub trait FoldersClientTrait: Send + Sync {
110///     async fn get(&self, id: FolderId) -> Result<FolderView, FolderError>;
111/// }
112/// ```
113#[proc_macro_attribute]
114pub fn client_trait(args: TokenStream, item: TokenStream) -> TokenStream {
115    let mut via_expr: Option<Expr> = None;
116    let parser = syn::meta::parser(|meta| {
117        if meta.path.is_ident("via") {
118            via_expr = Some(meta.value()?.parse::<Expr>()?);
119            Ok(())
120        } else {
121            Err(meta.error("expected `via = <expression>`"))
122        }
123    });
124
125    if let Err(e) = parser.parse(args) {
126        return e.to_compile_error().into();
127    }
128
129    let item_trait = parse_macro_input!(item as ItemTrait);
130    let Some(via_expr) = via_expr else {
131        return syn::Error::new_spanned(
132            &item_trait.ident,
133            "#[client_trait] requires a `via = <expression>` argument",
134        )
135        .to_compile_error()
136        .into();
137    };
138
139    let trait_ident = &item_trait.ident;
140    let expanded = quote! {
141        #item_trait
142
143        impl ::bitwarden_core::client::FromClientShared for dyn #trait_ident {
144            fn from_client_shared(
145                client: &::bitwarden_core::Client,
146            ) -> ::std::sync::Arc<Self> {
147                ::std::sync::Arc::new(#via_expr)
148            }
149        }
150    };
151    expanded.into()
152}