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}