log_derive/
lib.rs

1#![recursion_limit = "128"]
2
3//! # Log Derive
4//!
5//! `log-derive` provides a simple attribute macro that facilitates logs as part of the [`log`] facade <br>
6//! Right now it contains two macros [`logfn`], [`logfn_inputs`] these macros are only for functions but still have a lot of power.
7//!
8//!
9//!  # Use
10//! The basic use of these macros is by putting one or both of them on top of the function like this: `#[logfn(INFO)]` <br>
11//!
12//! The [`logfn`] macro is used to log the *output* of the function and [`logfn_inputs`] is used to log the *inputs*. <br>
13//! Please notice, the arguments being logged **must** implement the [`Debug`] trait. <br>
14//! (i.e. [`logfn`] requires the output to be [`Debug`] and [`logfn_inputs`] require the inputs to be [`Debug`]) <br>
15//!
16//! The macros will accept all log levels provided by the [`log`] facade. <br>
17//! In [`logfn`] if the function returns a [`Result`] type the macro will accept the following additional attributes: <br>
18//! `(ok = "LEVEL")` and `(err = "LEVEL")` this can provide different log levels if the function failed or not. <br>
19//!
20//! By default the macro uses the following formatting to print the message: <br>
21//! [`logfn`]: `("FUNCTION_NAME() => {:?}", return_val)` <br>
22//! [`logfn_inputs`]: `"FUNCTION_NAME(a: {:?}, b: {:?})", a, b)` <br>
23//! This can be easily changed using the `fmt` attribute: `#[logfn(LEVEL, fmt = "Important Result: {:}")` <br>
24//! which will accept format strings similar to [`println!`].
25//!
26//! [`logfn`]: ./attr.logfn.html
27//! [`logfn_inputs`]: ./attr.logfn_inputs.html
28//! [`log`]: https://docs.rs/log/latest/log/index.html
29//! [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
30//! [`println!`]: https://doc.rust-lang.org/stable/std/macro.println.html
31//! [`Debug`]: https://doc.rust-lang.org/std/fmt/trait.Debug.html
32//!
33//! ## Examples
34//! ```rust
35//! use log_derive::{logfn, logfn_inputs};
36//!
37//! # #[derive(Debug)]
38//! struct Error;
39//! # #[derive(Debug)]
40//! struct Success;
41//! # #[derive(Debug)]
42//! enum Status { Alive, Dead, Unknown }
43//!
44//! #[logfn(Warn)]
45//! #[logfn_inputs(Info, fmt = "Checking if {:?} is alive")]
46//! fn is_alive(person: &Person) -> Status {
47//!     # use Response::*;
48//!     # use Status::*;
49//!    match person.ping() {
50//!        Pong => Status::Alive,
51//!        Timeout => if person.is_awake() {
52//!            Unknown
53//!        } else {
54//!            Dead
55//!        }
56//!   }
57//!}
58//!
59//! #[logfn_inputs(Info)]
60//! #[logfn(ok = "TRACE", err = "ERROR")]
61//! fn call_isan(num: &str) -> Result<Success, Error> {
62//!     if num.len() >= 10 && num.len() <= 15 {
63//!         Ok(Success)
64//!     } else {
65//!         Err(Error)
66//!     }
67//! }
68//!
69//! #[logfn(INFO, fmt = "a + b = {}")]
70//! #[logfn_inputs(Trace, fmt = "adding a: {:?} and b: {:?}")]
71//! fn addition(a: usize, b: usize) -> usize {
72//!     a + b
73//! }
74//!
75//! #[logfn_inputs(Info)]
76//! #[logfn(ok = "TRACE", log_ts = true)]
77//! fn time_this(num: &str) -> Result<Success, Error> {
78//!     if num.len() >= 10 && num.len() <= 15 {
79//!        std::thread::sleep(Duration::from_secs(1));
80//!         Ok(Success)
81//!     } else {
82//!         Err(Error)
83//!     }
84//! }
85//!
86//! # enum Response {Pong, Timeout}
87//! # #[derive(Debug)]
88//! # struct Person;
89//! # impl Person {fn ping(&self) -> Response {Response::Pong}fn is_awake(&self) -> bool {true}}
90//! # use std::time::Duration;
91//! ```
92//!
93//!
94extern crate proc_macro;
95extern crate syn;
96use darling::{Error, FromMeta};
97use proc_macro2::TokenStream;
98use quote::{quote, ToTokens};
99
100use syn::punctuated::Punctuated;
101use syn::{
102    parse_macro_input, spanned::Spanned, token, AttributeArgs, Expr, ExprAsync, ExprAwait, ExprBlock, ExprCall, ExprClosure,
103    ExprParen, FnArg, Ident, ItemFn, Meta, NestedMeta, Pat, Result, ReturnType, Stmt, Type, TypePath,
104};
105
106struct FormattedAttributes {
107    ok_expr: TokenStream,
108    err_expr: TokenStream,
109    log_ts: bool,
110    contained_ok_or_err: bool,
111}
112
113impl FormattedAttributes {
114    pub fn parse_attributes(attr: &[NestedMeta], fmt_default: String) -> darling::Result<Self> {
115        OutputOptions::from_list(attr).map(|opts| Self::get_ok_err_streams(opts, fmt_default))
116    }
117
118    fn get_ok_err_streams(att: OutputOptions, fmt_default: String) -> Self {
119        let contained_ok_or_err = att.contains_ok_or_err();
120        let log_ts = att.log_ts();
121        let ok_log = att.ok_log();
122        let err_log = att.err_log();
123        let mut fmt = att.fmt().unwrap_or(fmt_default);
124        if log_ts {
125            fmt += ", ts={:#?}"
126        };
127
128        let ok_expr = match ok_log {
129            Some(loglevel) => {
130                let log_token = get_logger_token(&loglevel);
131                if log_ts {
132                    quote! {log::log!(#log_token, #fmt, result, ts); }
133                } else {
134                    quote! {log::log!(#log_token, #fmt, result); }
135                }
136            }
137            None => quote! {()},
138        };
139
140        let err_expr = match err_log {
141            Some(loglevel) => {
142                let log_token = get_logger_token(&loglevel);
143                if log_ts {
144                    quote! {log::log!(#log_token, #fmt, err, ts); }
145                } else {
146                    quote! {log::log!(#log_token, #fmt, err); }
147                }
148            }
149            None => quote! {()},
150        };
151        FormattedAttributes { ok_expr, err_expr, log_ts, contained_ok_or_err }
152    }
153}
154
155#[derive(Default, FromMeta)]
156#[darling(default)]
157struct OutputNamedOptions {
158    ok: Option<Ident>,
159    err: Option<Ident>,
160    fmt: Option<String>,
161    log_ts: Option<bool>,
162}
163
164struct OutputOptions {
165    /// The log level specified as the first word in the attribute.
166    leading_level: Option<Ident>,
167    named: OutputNamedOptions,
168}
169
170struct InputOptions {
171    level: Ident,
172    fmt: Option<String>,
173}
174
175impl FromMeta for InputOptions {
176    fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
177        let level;
178        let mut fmt = None;
179        if items.is_empty() {
180            return Err(Error::too_few_items(1));
181        }
182
183        match &items[0] {
184            NestedMeta::Meta(first) => {
185                if let Meta::Path(path) = first {
186                    if let Some(ident) = path.get_ident() {
187                        level = ident.clone();
188                    } else {
189                        return Err(Error::unexpected_type("first item should be a log level"));
190                    }
191                } else {
192                    return Err(Error::unexpected_type("first item should be a log level"));
193                }
194            }
195            NestedMeta::Lit(lit) => return Err(Error::unexpected_lit_type(lit)),
196        }
197
198        if items.len() > 1 {
199            fmt = String::from_nested_meta(&items[1]).ok();
200        }
201
202        Ok(InputOptions { level, fmt })
203    }
204}
205
206impl OutputOptions {
207    pub fn ok_log(&self) -> Option<&Ident> {
208        self.named.ok.as_ref().or_else(|| self.leading_level.as_ref())
209    }
210
211    pub fn err_log(&self) -> Option<&Ident> {
212        self.named.err.as_ref().or_else(|| self.leading_level.as_ref())
213    }
214
215    pub fn contains_ok_or_err(&self) -> bool {
216        self.named.ok.is_some() || self.named.err.is_some()
217    }
218
219    pub fn log_ts(&self) -> bool {
220        self.named.log_ts.unwrap_or(false)
221    }
222
223    pub fn fmt(&self) -> Option<String> {
224        self.named.fmt.clone()
225    }
226}
227
228impl FromMeta for OutputOptions {
229    fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
230        if items.is_empty() {
231            return Err(darling::Error::too_few_items(1));
232        }
233
234        let mut leading_level = None;
235
236        if let NestedMeta::Meta(first) = &items[0] {
237            if let Meta::Path(path) = first {
238                leading_level = path.get_ident().cloned();
239            }
240        }
241
242        let named =
243            if leading_level.is_some() { OutputNamedOptions::from_list(&items[1..])? } else { OutputNamedOptions::from_list(items)? };
244
245        Ok(OutputOptions { leading_level, named })
246    }
247}
248
249/// Check if a return type is some form of `Result`. This assumes that all types named `Result`
250/// are in fact results, but is resilient to the possibility of `Result` types being referenced
251/// from specific modules.
252pub(crate) fn is_result_type(ty: &TypePath) -> bool {
253    if let Some(segment) = ty.path.segments.iter().last() {
254        segment.ident == "Result"
255    } else {
256        false
257    }
258}
259
260fn check_if_return_result(f: &ItemFn) -> bool {
261    if let ReturnType::Type(_, t) = &f.sig.output {
262        return match t.as_ref() {
263            Type::Path(path) => is_result_type(path),
264            _ => false,
265        };
266    }
267
268    false
269}
270
271fn get_logger_token(att: &Ident) -> TokenStream {
272    // Capitalize the first letter.
273    let attr_str = att.to_string().to_lowercase();
274    let mut attr_char = attr_str.chars();
275    let attr_str = attr_char.next().unwrap().to_uppercase().to_string() + attr_char.as_str();
276    let att_str = Ident::new(&attr_str, att.span());
277    quote!(log::Level::#att_str)
278}
279
280fn make_closure(original: &ItemFn) -> Expr {
281    match original.sig.asyncness {
282        Some(asyncness) => Expr::Await(ExprAwait {
283            attrs: Default::default(),
284            await_token: Default::default(),
285            dot_token: Default::default(),
286            base: Box::new(syn::Expr::Async(ExprAsync {
287                attrs: Default::default(),
288                capture: Some(token::Move { span: original.span() }),
289                block: *original.block.clone(),
290                async_token: asyncness,
291            })),
292        }),
293        None => Expr::Call(ExprCall {
294            attrs: Default::default(),
295            args: Default::default(),
296            paren_token: Default::default(),
297            func: Box::new(syn::Expr::Paren(ExprParen {
298                attrs: Default::default(),
299                paren_token: Default::default(),
300                expr: Box::new(syn::Expr::Closure(ExprClosure {
301                    attrs: Default::default(),
302                    asyncness: Default::default(),
303                    movability: Default::default(),
304                    capture: Some(token::Move { span: original.span() }),
305                    or1_token: Default::default(),
306                    inputs: Default::default(),
307                    or2_token: Default::default(),
308                    output: ReturnType::Default,
309                    body: Box::new(Expr::Block(ExprBlock {
310                        attrs: Default::default(),
311                        label: Default::default(),
312                        block: *original.block.clone(),
313                    })),
314                })),
315            })),
316        }),
317    }
318}
319
320fn replace_function_headers(original: ItemFn, new: &mut ItemFn) {
321    let block = new.block.clone();
322    *new = original;
323    new.block = block;
324}
325
326fn generate_function(closure: &Expr, expressions: FormattedAttributes, result: bool) -> Result<ItemFn> {
327    let FormattedAttributes { ok_expr, err_expr, log_ts, contained_ok_or_err } = expressions;
328    let result = result || contained_ok_or_err;
329    let code = if log_ts {
330        if result {
331            quote! {
332                fn temp() {
333                    let instant = std::time::Instant::now();
334                    let result = #closure;
335                    let ts = instant.elapsed();
336                    result.map(|result| { #ok_expr; result })
337                        .map_err(|err| { #err_expr; err })
338                }
339            }
340        } else {
341            quote! {
342                fn temp() {
343                    let instant = std::time::Instant::now();
344                    let result = #closure;
345                    let ts = instant.elapsed();
346                    #ok_expr;
347                    result
348                }
349            }
350        }
351    } else if result {
352        quote! {
353            fn temp() {
354                let result = #closure;
355                result.map(|result| { #ok_expr; result })
356                    .map_err(|err| { #err_expr; err })
357            }
358        }
359    } else {
360        quote! {
361            fn temp() {
362                let result = #closure;
363                #ok_expr;
364                result
365            }
366        }
367    };
368
369    syn::parse2(code)
370}
371
372/// Logs the result of the function it's above.
373/// # Examples
374/// ``` rust
375///  # #[macro_use] extern crate log_derive;
376/// # use std::{net::*, io::{self, Write}};
377/// #[logfn(err = "Error", fmt = "Failed Sending Packet: {:?}")]
378/// fn send_hi(addr: SocketAddr) -> Result<(), io::Error> {
379///     let mut stream = TcpStream::connect(addr)?;
380///     stream.write(b"Hi!")?;
381///     Ok( () )
382/// }
383///
384///
385/// ```
386#[proc_macro_attribute]
387pub fn logfn(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
388    let attr = parse_macro_input!(attr as AttributeArgs);
389    let original_fn: ItemFn = parse_macro_input!(item as ItemFn);
390    let fmt_default = original_fn.sig.ident.to_string() + "() => {:?}";
391    let parsed_attributes: FormattedAttributes = match FormattedAttributes::parse_attributes(&attr, fmt_default) {
392        Ok(val) => val,
393        Err(err) => {
394            return err.write_errors().into();
395        }
396    };
397    let closure = make_closure(&original_fn);
398    let is_result = check_if_return_result(&original_fn);
399    let mut new_fn = generate_function(&closure, parsed_attributes, is_result).expect("Failed Generating Function");
400    replace_function_headers(original_fn, &mut new_fn);
401    new_fn.into_token_stream().into()
402}
403
404/// Logs the inputs of the function
405/// # Examples
406/// ``` rust
407///  # #[macro_use] extern crate log_derive;
408/// # use std::{net::*, io::{self, Write}};
409/// #[logfn_inputs(INFO, fmt = "Good morning: {:?}, to: {:?}")]
410/// fn good_morning(msg: &str, addr: SocketAddr) -> Result<(), io::Error> {
411///     let mut stream = TcpStream::connect(addr)?;
412///     stream.write(msg.as_bytes())?;
413///     Ok( () )
414/// }
415///
416///
417/// ```
418#[proc_macro_attribute]
419pub fn logfn_inputs(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
420    let mut original_fn: ItemFn = parse_macro_input!(item as ItemFn);
421
422    let attr = parse_macro_input!(attr as AttributeArgs);
423    let parsed_attributes = match InputOptions::from_list(&attr) {
424        Ok(val) => val,
425        Err(err) => {
426            return err.write_errors().into();
427        }
428    };
429
430    let mut stmts = match log_fn_inputs(&original_fn, parsed_attributes) {
431        Ok(input_log) => vec![input_log],
432        Err(e) => return e.to_compile_error().into(),
433    };
434
435    stmts.extend(original_fn.block.stmts);
436    original_fn.block.stmts = stmts;
437    original_fn.into_token_stream().into()
438}
439
440fn log_fn_inputs(func: &ItemFn, attr: InputOptions) -> syn::Result<Stmt> {
441    let fn_name = func.sig.ident.to_string();
442    let inputs: Vec<Ident> = func
443        .sig
444        .inputs
445        .iter()
446        .cloned()
447        .map(|arg| match arg {
448            FnArg::Receiver(arg) => arg.self_token.into(),
449            FnArg::Typed(pat_type) => {
450                if let Pat::Ident(ident) = *pat_type.pat {
451                    ident.ident
452                } else {
453                    unimplemented!()
454                }
455            }
456        })
457        .collect();
458
459    let items: Punctuated<_, token::Comma> = inputs.iter().cloned().collect();
460
461    let level = get_logger_token(&attr.level);
462    let fmt = attr.fmt.unwrap_or_else(|| {
463        let mut fmt = String::with_capacity(inputs.len() * 9);
464        fmt.push_str(&fn_name);
465        fmt.push('(');
466
467        for input in inputs {
468            fmt.push_str(&input.to_string());
469            fmt.push_str(": {:?},");
470        }
471        fmt.pop(); // Remove the extra comma.
472        fmt.push(')');
473        fmt
474    });
475
476    let res = quote! {
477        log::log!(#level, #fmt, #items);
478    };
479    syn::parse2(res)
480}
481
482#[cfg(test)]
483mod tests {
484    use syn::parse_quote;
485
486    use super::is_result_type;
487
488    #[test]
489    fn result_type() {
490        assert!(is_result_type(&parse_quote!(Result<T, E>)));
491        assert!(is_result_type(&parse_quote!(std::result::Result<T, E>)));
492        assert!(is_result_type(&parse_quote!(fmt::Result)));
493    }
494}