Skip to main content

ldap_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use convert_case::{Case, Casing as _};
4use syn::spanned::Spanned as _;
5
6/// Performs an LDAP search and converts the result into suitable Rust types
7///
8/// the first few parameters are the same as in the function version of ldap_search
9/// from the ldap-utils crate (and indeed they are just passed through to that)
10///
11/// Where this starts to differ is the attribute list, each attribute name has
12/// a Rust type, either a Vec (for multi-valued attributes), an Option
13/// (for optional single-valued attributes) or a bare type implementing the
14/// `ldap_types::conversion::FromStringLdapType` trait for values converting
15/// from the string attributes or each of those wrapped in
16/// `ldap_types::conversion::Binary` and implementing
17/// `ldap_types::conversion::FromBinaryLdapType` for values converting from
18/// binary attributes.
19///
20/// Unwrapping the Binary part needs to happen manually for now.
21///
22/// The attribute names are converted to snake case for variable names and under
23/// the hood an async function is generated with the specified return type and
24/// body. In addition to the attributes the function also gets a parameter dn
25/// for the entry DN as a `ldap_types::basic::DistinguishedName`
26///
27///
28/// ```
29/// #[tokio::main]
30/// pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
31///     let (mut ldap, base_dn) = ldap_utils::connect().await?;
32///     ldap_macros::ldap_search!(
33///         &mut ldap,
34///         &base_dn,
35///         ldap3::Scope::Subtree,
36///         "(objectclass=fooBar)",
37///         [ "fooAttribute as usize", "barAttribute as Option<bool>", "bazAttribute as Vec<String>", "quuxAttribute as ldap_types::conversion::Binary<Vec<u8>>" ],
38///         "Result<(), Box<dyn std::error::Error>>",
39///         {
40///             let ldap_types::conversion::Binary(quux_attribute) = quux_attribute;
41///             println!("DN: {}, foo: {}, bar: {:?}, baz: {:?}, quux: {:?}", dn, foo_attribute, bar_attribute, baz_attribute, quux_attribute);
42///             Ok(())
43///         }
44///     );
45///     Ok(())
46/// }
47/// ```
48#[proc_macro]
49pub fn ldap_search(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
50    let args = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
51
52    let Some(ldap_client_handle) = args.get(0) else {
53        return quote::quote_spanned! { args.span() =>
54            compile_error!("Missing first argument: ldap client handle");
55        }
56        .into();
57    };
58    let Some(base_dn) = args.get(1) else {
59        return quote::quote_spanned! { args.span() =>
60            compile_error!("Missing second argument: base dn");
61        }
62        .into();
63    };
64    let Some(scope) = args.get(2) else {
65        return quote::quote_spanned! { args.span() =>
66            compile_error!("Missing third argument: scope");
67        }
68        .into();
69    };
70    let Some(filter) = args.get(3) else {
71        return quote::quote_spanned! { args.span() =>
72            compile_error!("Missing fourth argument: filter");
73        }
74        .into();
75    };
76    let Some(attributes) = args.get(4) else {
77        return quote::quote_spanned! { args.span() =>
78            compile_error!("Missing fifth argument: attributes (array of attribute specifiers)");
79        }
80        .into();
81    };
82    let Some(return_type) = args.get(5) else {
83        return quote::quote_spanned! { args.span() =>
84            compile_error!("Missing sixth argument: return type (literal String)");
85        }
86        .into();
87    };
88    let Some(body) = args.get(6) else {
89        return quote::quote_spanned! { args.span() =>
90            compile_error!("Missing seventh argument: body (code block)");
91        }
92        .into();
93    };
94
95    let syn::Expr::Array(attributes) = attributes else {
96        return quote::quote! { compile_error!("Expected fifth argument to be an array of attribute specifiers (attribute name as Rust type)") }.into();
97    };
98
99    let syn::Expr::Lit(syn::ExprLit {
100        attrs: _,
101        lit: syn::Lit::Str(return_type),
102    }) = return_type
103    else {
104        return quote::quote! { compile_error!("Expected sixth argument to be a literal String containing a Rust type") }.into();
105    };
106
107    let Ok(return_type): Result<syn::Type, syn::Error> = syn::parse_str(&return_type.value())
108    else {
109        return quote::quote! { compile_error!("Expected sixth argument to be a literal String containing a Rust type") }.into();
110    };
111
112    let mut attribute_names = Vec::new();
113    let mut attribute_handlers = Vec::new();
114    let mut attribute_definition_parameters = Vec::new();
115    let mut attribute_call_parameters = Vec::new();
116    attribute_definition_parameters.push(quote::quote! {
117        dn: ldap_types::basic::DistinguishedName
118    });
119    attribute_call_parameters.push(quote::quote! {
120        dn
121    });
122    for elem in &attributes.elems {
123        let span = elem.span();
124        let syn::Expr::Lit(syn::ExprLit {
125            attrs: _,
126            lit: syn::Lit::Str(attribute_specifier),
127        }) = elem
128        else {
129            return quote::quote! { compile_error!("Expected attribute specifier to be literal string") }.into();
130        };
131        let Ok(attribute_cast): Result<syn::ExprCast, syn::Error> =
132            syn::parse_str(&attribute_specifier.value())
133        else {
134            return quote::quote! { compile_error!("Expected attribute specifier to be cast expression") }.into();
135        };
136        let syn::Expr::Path(syn::ExprPath {
137            attrs: _,
138            qself: _,
139            path:
140                syn::Path {
141                    leading_colon: None,
142                    segments,
143                },
144        }) = *attribute_cast.expr
145        else {
146            return quote::quote! { compile_error!("Expected attribute name to be identifier (within the literal string for the cast expression)") }.into();
147        };
148        if segments.len() != 1 {
149            return quote::quote! { compile_error!("Expected attribute name to be identifier with a path length of 1") }.into();
150        }
151        let Some(attribute_name) = segments.first().map(|s| s.ident.to_string()) else {
152            return quote::quote! { compile_error!("Expected attribute name to be identifier with a path length of 1") }.into();
153        };
154        let attribute_rust_type = *attribute_cast.ty;
155        let attribute_rust_variable = syn::Ident::new(&attribute_name.to_case(Case::Snake), span);
156        attribute_names.push(quote::quote! {
157            #attribute_name
158        });
159        attribute_handlers.push(quote::quote! {
160            let #attribute_rust_variable: #attribute_rust_type =
161                <#attribute_rust_type as ldap_types::conversion::FromLdapType>::parse(<ldap3::SearchEntry as ldap_types::conversion::SearchEntryExt>::attribute_results(&entry, #attribute_name))?;
162        });
163        attribute_definition_parameters.push(quote::quote! {
164            #attribute_rust_variable: #attribute_rust_type
165        });
166        attribute_call_parameters.push(quote::quote! {
167            #attribute_rust_variable
168        });
169    }
170
171    let output = quote::quote! {
172        let it = ldap_utils::ldap_search(
173            #ldap_client_handle,
174            #base_dn,
175            #scope,
176            #filter,
177            vec![#(#attribute_names),*],
178        ).await?;
179
180        // Collect into a Vec to remove the lifetime dependency and make the future Send
181        let entries: Vec<ldap3::SearchEntry> = it.collect();
182
183        let mut generated_ldap_search_entry_handler = async |#(#attribute_definition_parameters),*| -> #return_type #body;
184
185        for entry in entries { // Iterate over the collected Vec
186            let dn : ldap_types::basic::DistinguishedName = entry.dn.clone().try_into()?;
187            #(#attribute_handlers)*
188            generated_ldap_search_entry_handler(#(#attribute_call_parameters),*).await?;
189        }
190    };
191
192    //println!("Macro output:\n{}", output);
193
194    proc_macro::TokenStream::from(output)
195}