Skip to content

Commit

Permalink
Support arbitrary ordering of optional arguments for logging macros
Browse files Browse the repository at this point in the history
  • Loading branch information
SpriteOvO committed Nov 26, 2024
1 parent eccadee commit 1a8f5da
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 80 deletions.
41 changes: 36 additions & 5 deletions spdlog-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
//!
//! [`spdlog-rs`]: https://crates.io/crates/spdlog-rs
mod normalize_forward;
mod pattern;

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use spdlog_internal::pattern_parser::Result;
use quote::quote;

#[proc_macro]
pub fn pattern(input: TokenStream) -> TokenStream {
let pattern = syn::parse_macro_input!(input);
into_or_error(pattern::pattern_impl(pattern))
into_or_error(pattern::pattern_impl(pattern).map_err(Error::PatternParser))
}

#[proc_macro]
Expand All @@ -23,7 +24,7 @@ pub fn runtime_pattern(input: TokenStream) -> TokenStream {
// token which is used in the custom patterns.

let runtime_pattern = syn::parse_macro_input!(input);
into_or_error(pattern::runtime_pattern_impl(runtime_pattern))
into_or_error(pattern::runtime_pattern_impl(runtime_pattern).map_err(Error::PatternParser))
}

#[proc_macro]
Expand All @@ -33,9 +34,39 @@ pub fn runtime_pattern_disabled(_: TokenStream) -> TokenStream {
);
}

fn into_or_error(result: Result<TokenStream2>) -> TokenStream {
// Example:
//
// ```rust
// normalize_forward!(callback => default[opt1: 1, opt2: {}, opt3: { 3 }, d], opt1: 10, a, b, c, opt3: { 30 });
// // will be converted to
// spdlog::callback!(opt1: 10, opt2: {}, opt3: { 30 }, d, a, b, c);
// ```
#[proc_macro]
pub fn normalize_forward(input: TokenStream) -> TokenStream {
let normalize = syn::parse_macro_input!(input);
into_or_error(normalize_forward::normalize(normalize).map_err(Error::NormalizeForward))
}

enum Error {
PatternParser(spdlog_internal::pattern_parser::Error),
NormalizeForward(syn::Error),
}

impl Error {
fn emit(self) -> TokenStream2 {
match self {
Error::PatternParser(err) => {
let error = err.to_string();
quote!(compile_error!(#error))
}
Error::NormalizeForward(err) => err.to_compile_error(),
}
}
}

fn into_or_error(result: Result<TokenStream2, Error>) -> TokenStream {
match result {
Ok(stream) => stream.into(),
Err(err) => panic!("{}", err),
Err(err) => err.emit().into(),
}
}
239 changes: 239 additions & 0 deletions spdlog-macros/src/normalize_forward.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
use std::collections::HashSet;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
braced, bracketed,
parse::{discouraged::Speculative, Parse, ParseStream},
Expr, Ident, Token,
};

pub struct Normalize {
callback: Ident,
default_list: DefaultArgsList,
args: Args,
}

impl Parse for Normalize {
fn parse(input: ParseStream) -> syn::Result<Self> {
let callback = input.parse::<Ident>()?;
input.parse::<Token![=>]>()?;
let default_list = DefaultArgsList::parse(input)?;
input.parse::<Token![,]>()?;
let args = Args::parse(input)?;
Ok(Self {
callback,
default_list,
args,
})
}
}

struct DefaultArgsList(Vec<Arg>);

impl Parse for DefaultArgsList {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<Token![default]>()?;
let list;
bracketed!(list in input);
let list = list
.parse_terminated(Arg::parse, Token![,])?
.into_iter()
.collect();
Ok(Self(list))
}
}

struct Args(Vec<Arg>);

impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args = input.parse_terminated(Arg::parse, Token![,])?;
Ok(Self(args.into_iter().collect()))
}
}

enum Arg {
Optional(OptionalArg),
Other(ArgValue),
}

impl Arg {
fn as_optional(&self) -> Option<&OptionalArg> {
match self {
Self::Optional(arg) => Some(arg),
_ => None,
}
}

fn as_optional_mut(&mut self) -> Option<&mut OptionalArg> {
match self {
Self::Optional(arg) => Some(arg),
_ => None,
}
}
}

impl Parse for Arg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let fork = input.fork();
match OptionalArg::parse(&fork) {
Ok(opt_arg) => {
input.advance_to(&fork);
Ok(Self::Optional(opt_arg))
}
Err(_) => Ok(Self::Other(ArgValue::parse(input)?)),
}
}
}

struct OptionalArg {
name: Ident,
value: ArgValue,
}

impl Parse for OptionalArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse::<Ident>()?;
input.parse::<Token![:]>()?;
let value = ArgValue::parse(input)?;
Ok(Self { name, value })
}
}

enum ArgValue {
Expr(Expr),
Braced(BraceAny),
}

impl ArgValue {
fn into_token_stream(self) -> TokenStream {
match self {
Self::Expr(expr) => expr.into_token_stream(),
Self::Braced(braced) => braced.0,
}
}
}

impl Parse for ArgValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let fork = input.fork();

match Expr::parse(&fork) {
Ok(expr) => {
input.advance_to(&fork);
Ok(Self::Expr(expr))
}
Err(_) => Ok(BraceAny::parse(input).map(Self::Braced)?),
}
}
}

struct BraceAny(TokenStream);

impl Parse for BraceAny {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
braced!(content in input);
let ts: TokenStream = content.parse()?;
Ok(Self(quote!({#ts})))
}
}

fn check_inputs(normalize: &Normalize) -> syn::Result<()> {
let mut seen_keys = HashSet::new();
for arg in normalize.args.0.iter().filter_map(|arg| arg.as_optional()) {
if !seen_keys.insert(&arg.name) {
return Err(syn::Error::new(
arg.name.span(),
format!("found duplicate optional argument '{}'", arg.name),
));
}
}

let groups = normalize
.args
.0
.split_inclusive(|arg| matches!(arg, Arg::Optional(_)))
.filter(|group| {
group
.iter()
.filter(|arg| !matches!(arg, Arg::Optional(_)))
.next()
.is_some()
})
.collect::<Vec<_>>();
if groups.len() > 1 {
return Err(syn::Error::new(
groups
.first()
.and_then(|group| group.last())
.and_then(|arg| arg.as_optional())
.unwrap()
.name
.span(),
"optional arguments cannot occur in the middle of regular arguments",
));
}

Ok(())
}

pub fn normalize(normalize: Normalize) -> syn::Result<TokenStream> {
check_inputs(&normalize)?;

let mut default_args = normalize.default_list.0;
let mut other_args = vec![];

for input_arg in normalize.args.0 {
match input_arg {
Arg::Optional(input_arg) => {
let stored = default_args
.iter_mut()
.find_map(|allowed| {
allowed
.as_optional_mut()
.and_then(|allowed| (allowed.name == input_arg.name).then_some(allowed))
})
.ok_or_else(|| {
syn::Error::new(
input_arg.name.span(),
format!("unknown optional parameter '{}'", input_arg.name),
)
})?;
stored.value = input_arg.value;
}
Arg::Other(input_arg) => {
other_args.push(input_arg);
}
}
}

let callback = normalize.callback;
let default_args = default_args
.into_iter()
.map(|arg| match arg {
Arg::Optional(arg) => {
let name = arg.name;
let value = arg.value.into_token_stream();
quote!(#name: #value)
}
Arg::Other(arg) => {
let value = arg.into_token_stream();
quote!(#value)
}
})
.collect::<Vec<_>>();
let other_args = other_args
.into_iter()
.map(|arg| {
let ts = arg.into_token_stream();
quote!(#ts)
})
.collect::<Vec<_>>();

let emitted = quote! {
::spdlog::#callback!(#(#default_args),*, #(#other_args),*)
};
Ok(emitted)
}
6 changes: 6 additions & 0 deletions spdlog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@
#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
#![warn(missing_docs)]

// Used for referencing from proc-macros
// Credits: https://stackoverflow.com/a/57049687
extern crate self as spdlog;

mod env_level;
pub mod error;
pub mod formatter;
Expand Down Expand Up @@ -308,6 +312,8 @@ pub use log_crate_proxy::*;
pub use logger::*;
pub use record::*;
pub use source_location::*;
#[doc(hidden)]
pub use spdlog_macros::normalize_forward as __normalize_forward;
pub use string_buf::StringBuf;
#[cfg(feature = "multi-thread")]
pub use thread_pool::*;
Expand Down
Loading

0 comments on commit 1a8f5da

Please sign in to comment.