From 108696f7a4cbffa3acb66e10a88c2cc2d6dc7171 Mon Sep 17 00:00:00 2001 From: Asuna Date: Thu, 8 Jun 2023 21:09:50 +0800 Subject: [PATCH 1/4] Split pattern template parser from `spdlog-macros` to `spdlog-internal` --- Cargo.toml | 1 + spdlog-internal/Cargo.toml | 11 + spdlog-internal/src/lib.rs | 12 + spdlog-internal/src/pattern_parser/error.rs | 106 ++++ .../src/pattern_parser}/helper.rs | 0 spdlog-internal/src/pattern_parser/mod.rs | 144 ++++++ spdlog-internal/src/pattern_parser/parse.rs | 374 +++++++++++++++ .../src/pattern_parser/registry.rs | 91 ++++ spdlog-macros/Cargo.toml | 2 + spdlog-macros/src/lib.rs | 26 +- spdlog-macros/src/parse.rs | 454 ------------------ spdlog-macros/src/pattern/mod.rs | 109 +++++ spdlog-macros/src/pattern/synthesis.rs | 96 ++++ spdlog-macros/src/synthesis.rs | 301 ------------ 14 files changed, 949 insertions(+), 778 deletions(-) create mode 100644 spdlog-internal/Cargo.toml create mode 100644 spdlog-internal/src/lib.rs create mode 100644 spdlog-internal/src/pattern_parser/error.rs rename {spdlog-macros/src => spdlog-internal/src/pattern_parser}/helper.rs (100%) create mode 100644 spdlog-internal/src/pattern_parser/mod.rs create mode 100644 spdlog-internal/src/pattern_parser/parse.rs create mode 100644 spdlog-internal/src/pattern_parser/registry.rs delete mode 100644 spdlog-macros/src/parse.rs create mode 100644 spdlog-macros/src/pattern/mod.rs create mode 100644 spdlog-macros/src/pattern/synthesis.rs delete mode 100644 spdlog-macros/src/synthesis.rs diff --git a/Cargo.toml b/Cargo.toml index f3767d99..c3e8e358 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ resolver = "2" members = [ "spdlog", + "spdlog-internal", "spdlog-macros", ] diff --git a/spdlog-internal/Cargo.toml b/spdlog-internal/Cargo.toml new file mode 100644 index 00000000..b6882814 --- /dev/null +++ b/spdlog-internal/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "spdlog-internal" +version = "0.1.0" +edition = "2021" +rust-version = "1.56" + +[dependencies] +nom = "7.1.3" +strum = { version = "0.24.1", features = ["derive"] } +strum_macros = "0.24.3" +thiserror = "1.0.40" diff --git a/spdlog-internal/src/lib.rs b/spdlog-internal/src/lib.rs new file mode 100644 index 00000000..cbf9a846 --- /dev/null +++ b/spdlog-internal/src/lib.rs @@ -0,0 +1,12 @@ +pub mod pattern_parser; + +#[macro_export] +macro_rules! impossible { + ( $dbg_lit:literal, $($fmt_arg:expr),* ) => { + panic!( + "this should not happen, please open an issue on 'spdlog-rs' Bug Tracker\n\nsource: {}\ndebug:{}", + format!("{}:{}", file!(), line!()), + format!($dbg_lit, $($fmt_arg),*), + ) + }; +} diff --git a/spdlog-internal/src/pattern_parser/error.rs b/spdlog-internal/src/pattern_parser/error.rs new file mode 100644 index 00000000..115090fa --- /dev/null +++ b/spdlog-internal/src/pattern_parser/error.rs @@ -0,0 +1,106 @@ +use std::fmt::{self, Display}; + +use nom::error::Error as NomError; +use thiserror::Error; + +use super::PatternKind; +use crate::impossible; + +#[derive(Error, Debug)] +pub enum Error { + ConflictName { + existing: PatternKind<()>, + incoming: PatternKind<()>, + }, + Template(TemplateError), + Parse(NomError), +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::ConflictName { existing, incoming } => match (existing, incoming) { + (PatternKind::BuiltIn(_), PatternKind::Custom { .. }) => { + write!( + f, + "'{}' is already a built-in pattern, please try another name", + existing.placeholder() + ) + } + (PatternKind::Custom { .. }, PatternKind::Custom { .. }) => { + write!( + f, + "the constructor of custom pattern '{}' is specified more than once", + existing.placeholder() + ) + } + (_, PatternKind::BuiltIn { .. }) => { + impossible!("{}", self) + } + }, + Error::Template(err) => { + write!(f, "template ill-format: {}", err) + } + Error::Parse(err) => { + write!(f, "failed to parse template string: {}", err) + } + } + } +} + +#[derive(Error, Debug)] +pub enum TemplateError { + WrongPatternKindReference { + is_builtin_as_custom: bool, + placeholder: String, + }, + UnknownPatternReference { + is_custom: bool, + placeholder: String, + }, + MultipleStyleRange, +} + +impl Display for TemplateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TemplateError::WrongPatternKindReference { + is_builtin_as_custom, + placeholder, + } => { + if *is_builtin_as_custom { + write!( + f, + "'{}' is a built-in pattern, it cannot be used as a custom pattern. try to replace it with `{{{}}}`", + placeholder, placeholder + ) + } else { + write!( + f, + "'{}' is a custom pattern, it cannot be used as a built-in pattern. try to replace it with `{{${}}}`", + placeholder, placeholder + ) + } + } + TemplateError::UnknownPatternReference { + is_custom, + placeholder, + } => { + if *is_custom { + write!( + f, + "the constructor of custom pattern '{}' is not specified", + placeholder + ) + } else { + write!(f, "no built-in pattern named '{}'", placeholder) + } + } + TemplateError::MultipleStyleRange => { + write!(f, "multiple style ranges are not currently supported") + } + } + } +} + +pub type Result = std::result::Result; diff --git a/spdlog-macros/src/helper.rs b/spdlog-internal/src/pattern_parser/helper.rs similarity index 100% rename from spdlog-macros/src/helper.rs rename to spdlog-internal/src/pattern_parser/helper.rs diff --git a/spdlog-internal/src/pattern_parser/mod.rs b/spdlog-internal/src/pattern_parser/mod.rs new file mode 100644 index 00000000..1052413e --- /dev/null +++ b/spdlog-internal/src/pattern_parser/mod.rs @@ -0,0 +1,144 @@ +use std::borrow::Cow; + +use strum::IntoEnumIterator; +use strum_macros::{EnumDiscriminants, EnumIter, EnumString, IntoStaticStr}; + +pub mod error; +mod helper; +pub mod parse; +mod registry; + +pub use error::{Error, Result}; +pub use registry::PatternRegistry; + +#[derive( + Clone, Copy, Debug, Eq, PartialEq, IntoStaticStr, EnumDiscriminants, EnumIter, EnumString, +)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum BuiltInFormatterInner { + #[strum(serialize = "weekday_name")] + AbbrWeekdayName, + #[strum(serialize = "weekday_name_full")] + WeekdayName, + #[strum(serialize = "month_name")] + AbbrMonthName, + #[strum(serialize = "month_name_full")] + MonthName, + #[strum(serialize = "datetime")] + FullDateTime, + #[strum(serialize = "year_short")] + ShortYear, + #[strum(serialize = "year")] + Year, + #[strum(serialize = "date_short")] + ShortDate, + #[strum(serialize = "date")] + Date, + #[strum(serialize = "month")] + Month, + #[strum(serialize = "day")] + Day, + #[strum(serialize = "hour")] + Hour, + #[strum(serialize = "hour_12")] + Hour12, + #[strum(serialize = "minute")] + Minute, + #[strum(serialize = "second")] + Second, + #[strum(serialize = "millisecond")] + Millisecond, + #[strum(serialize = "microsecond")] + Microsecond, + #[strum(serialize = "nanosecond")] + Nanosecond, + #[strum(serialize = "am_pm")] + AmPm, + #[strum(serialize = "time_12")] + Time12, + #[strum(serialize = "time_short")] + ShortTime, + #[strum(serialize = "time")] + Time, + #[strum(serialize = "tz_offset")] + TzOffset, + #[strum(serialize = "unix_timestamp")] + UnixTimestamp, + #[strum(serialize = "full")] + Full, + #[strum(serialize = "level")] + Level, + #[strum(serialize = "level_short")] + ShortLevel, + #[strum(serialize = "source")] + Source, + #[strum(serialize = "file_name")] + SourceFilename, + #[strum(serialize = "file")] + SourceFile, + #[strum(serialize = "line")] + SourceLine, + #[strum(serialize = "column")] + SourceColumn, + #[strum(serialize = "module_path")] + SourceModulePath, + #[strum(serialize = "logger")] + LoggerName, + #[strum(serialize = "payload")] + Payload, + #[strum(serialize = "pid")] + ProcessId, + #[strum(serialize = "tid")] + ThreadId, + #[strum(serialize = "eol")] + Eol, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BuiltInFormatter(BuiltInFormatterInner); + +impl BuiltInFormatter { + pub fn iter() -> impl Iterator { + BuiltInFormatterInner::iter().map(BuiltInFormatter) + } + + pub fn struct_name(&self) -> &'static str { + BuiltInFormatterInnerDiscriminants::from(self.0).into() + } + + pub fn placeholder(&self) -> &'static str { + self.0.into() + } + + pub fn inner(&self) -> BuiltInFormatterInner { + self.0 + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PatternKind { + BuiltIn(BuiltInFormatter), + Custom { + placeholder: Cow<'static, str>, + factory: F, + }, +} + +impl PatternKind { + pub(crate) fn placeholder(&self) -> &str { + match self { + PatternKind::BuiltIn(f) => f.placeholder(), + PatternKind::Custom { placeholder, .. } => placeholder, + } + } + + pub(crate) fn to_factory_erased(&self) -> PatternKind<()> { + match self { + PatternKind::BuiltIn(b) => PatternKind::BuiltIn(b.clone()), + PatternKind::Custom { placeholder, .. } => PatternKind::Custom { + placeholder: placeholder.clone(), + factory: (), + }, + } + } +} diff --git a/spdlog-internal/src/pattern_parser/parse.rs b/spdlog-internal/src/pattern_parser/parse.rs new file mode 100644 index 00000000..a37d289b --- /dev/null +++ b/spdlog-internal/src/pattern_parser/parse.rs @@ -0,0 +1,374 @@ +use nom::{error::Error as NomError, Parser}; + +use super::{helper, Error, Result}; + +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub struct Template<'a> { + pub tokens: Vec>, +} + +impl<'a> Template<'a> { + pub fn parse(template: &'a str) -> Result { + let mut parser = Self::parser(); + + let (_, parsed_template) = parser.parse(template).map_err(|err| { + let err = match err { + // The "complete" combinator should transform `Incomplete` into `Error` + nom::Err::Incomplete(..) => unreachable!(), + nom::Err::Error(err) | nom::Err::Failure(err) => err, + }; + Error::Parse(NomError::new(err.input.into(), err.code)) + })?; + + Ok(parsed_template) + } +} + +impl<'a> Template<'a> { + #[must_use] + fn parser() -> impl Parser<&'a str, Template<'a>, NomError<&'a str>> { + let token_parser = TemplateToken::parser(); + nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof)) + .map(|(tokens, _)| Self { tokens }) + } + + #[must_use] + fn parser_without_style_range() -> impl Parser<&'a str, Template<'a>, NomError<&'a str>> { + let token_parser = TemplateToken::parser_without_style_range(); + nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof)) + .map(|(tokens, _)| Self { tokens }) + } +} + +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub enum TemplateToken<'a> { + Literal(TemplateLiteral), + Formatter(TemplateFormatterToken<'a>), + StyleRange(TemplateStyleRange<'a>), +} + +impl<'a> TemplateToken<'a> { + #[must_use] + fn parser() -> impl Parser<&'a str, TemplateToken<'a>, NomError<&'a str>> { + let style_range_parser = TemplateStyleRange::parser(); + let other_parser = Self::parser_without_style_range(); + + nom::combinator::map(style_range_parser, Self::StyleRange).or(other_parser) + } + + #[must_use] + fn parser_without_style_range() -> impl Parser<&'a str, TemplateToken<'a>, NomError<&'a str>> { + let literal_parser = TemplateLiteral::parser(); + let formatter_parser = TemplateFormatterToken::parser(); + + nom::combinator::map(literal_parser, Self::Literal) + .or(nom::combinator::map(formatter_parser, Self::Formatter)) + } +} + +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub struct TemplateLiteral { + pub literal: String, +} + +impl TemplateLiteral { + #[must_use] + fn parser<'a>() -> impl Parser<&'a str, Self, NomError<&'a str>> { + let literal_char_parser = nom::combinator::value('{', nom::bytes::complete::tag("{{")) + .or(nom::combinator::value('}', nom::bytes::complete::tag("}}"))) + .or(nom::character::complete::none_of("{")); + nom::multi::many1(literal_char_parser).map(|literal_chars| Self { + literal: literal_chars.into_iter().collect(), + }) + } +} + +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub struct TemplateFormatterToken<'a> { + pub has_custom_prefix: bool, + pub placeholder: &'a str, +} + +impl<'a> TemplateFormatterToken<'a> { + #[must_use] + fn parser() -> impl Parser<&'a str, TemplateFormatterToken<'a>, NomError<&'a str>> { + let open_paren = nom::character::complete::char('{'); + let close_paren = nom::character::complete::char('}'); + let formatter_prefix = nom::character::complete::char('$'); + let formatter_placeholder = nom::combinator::recognize(nom::sequence::tuple(( + nom::combinator::opt(formatter_prefix), + nom::branch::alt(( + nom::character::complete::alpha1, + nom::bytes::complete::tag("_"), + )), + nom::multi::many0_count(nom::branch::alt(( + nom::character::complete::alphanumeric1, + nom::bytes::complete::tag("_"), + ))), + ))); + + nom::sequence::delimited(open_paren, formatter_placeholder, close_paren).map( + move |placeholder: &str| match placeholder.strip_prefix('$') { + Some(placeholder) => Self { + has_custom_prefix: true, + placeholder, + }, + None => Self { + has_custom_prefix: false, + placeholder, + }, + }, + ) + } +} + +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub struct TemplateStyleRange<'a> { + pub body: Template<'a>, +} + +impl<'a> TemplateStyleRange<'a> { + #[must_use] + fn parser() -> impl Parser<&'a str, TemplateStyleRange<'a>, NomError<&'a str>> { + nom::bytes::complete::tag("{^") + .and(helper::take_until_unbalanced('{', '}')) + .and(nom::bytes::complete::tag("}")) + .map(|((_, body), _)| body) + .and_then(Template::parser_without_style_range()) + .map(|body| Self { body }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod template_parsing { + use super::*; + + fn parse_template_str(template: &str) -> nom::IResult<&str, Template> { + Template::parser().parse(template) + } + + #[test] + fn test_parse_basic() { + assert_eq!( + parse_template_str(r#"hello"#), + Ok(( + "", + Template { + tokens: vec![TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello"), + }),], + } + )) + ); + } + + #[test] + fn test_parse_empty() { + assert_eq!( + parse_template_str(""), + Ok(("", Template { tokens: Vec::new() },)) + ); + } + + #[test] + fn test_parse_escape_literal() { + assert_eq!( + parse_template_str(r#"hello {{name}}"#), + Ok(( + "", + Template { + tokens: vec![TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello {name}"), + }),], + } + )) + ); + } + + #[test] + fn test_parse_escape_literal_at_beginning() { + assert_eq!( + parse_template_str(r#"{{name}}"#), + Ok(( + "", + Template { + tokens: vec![TemplateToken::Literal(TemplateLiteral { + literal: String::from("{name}"), + }),], + } + )) + ); + } + + #[test] + fn test_parse_formatter_basic() { + assert_eq!( + parse_template_str(r#"hello {full}!{$custom}"#), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello "), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: false, + placeholder: "full" + }), + TemplateToken::Literal(TemplateLiteral { + literal: String::from("!"), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: true, + placeholder: "custom", + }), + ], + } + )) + ); + + assert_eq!( + parse_template_str(r#"hello {not_exists}!{$custom}"#), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello "), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: false, + placeholder: "not_exists", + }), + TemplateToken::Literal(TemplateLiteral { + literal: String::from("!"), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: true, + placeholder: "custom", + }), + ], + } + )) + ); + } + + #[test] + fn test_parse_literal_single_close_paren() { + assert_eq!( + parse_template_str(r#"hello name}"#), + Ok(( + "", + Template { + tokens: vec![TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello name}"), + }),], + } + )) + ); + } + + #[test] + fn test_parse_formatter_invalid_name() { + assert!(parse_template_str(r#"hello {name{}!"#).is_err()); + } + + #[test] + fn test_parse_formatter_missing_close_paren() { + assert!(parse_template_str(r#"hello {name"#).is_err()); + } + + #[test] + fn test_parse_formatter_duplicate_close_paren() { + assert_eq!( + parse_template_str(r#"hello {time}}"#), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello "), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: false, + placeholder: "time", + }), + TemplateToken::Literal(TemplateLiteral { + literal: String::from("}"), + }), + ], + } + )) + ); + } + + #[test] + fn test_parse_style_range_basic() { + assert_eq!( + parse_template_str(r#"hello {^world}"#), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello "), + }), + TemplateToken::StyleRange(TemplateStyleRange { + body: Template { + tokens: vec![TemplateToken::Literal(TemplateLiteral { + literal: String::from("world"), + }),], + }, + }), + ], + } + )) + ); + + assert_eq!( + parse_template_str(r#"hello {^world {level} {$c_pat} {{escape}}}"#), + Ok(( + "", + Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("hello "), + }), + TemplateToken::StyleRange(TemplateStyleRange { + body: Template { + tokens: vec![ + TemplateToken::Literal(TemplateLiteral { + literal: String::from("world "), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: false, + placeholder: "level", + }), + TemplateToken::Literal(TemplateLiteral { + literal: String::from(" "), + }), + TemplateToken::Formatter(TemplateFormatterToken { + has_custom_prefix: true, + placeholder: "c_pat", + }), + TemplateToken::Literal(TemplateLiteral { + literal: String::from(" {escape}"), + }), + ], + }, + }), + ], + } + )) + ); + } + + #[test] + fn test_parse_style_range_nested() { + assert!(parse_template_str(r#"hello {^ hello {^ world } }"#).is_err()); + } + } +} diff --git a/spdlog-internal/src/pattern_parser/registry.rs b/spdlog-internal/src/pattern_parser/registry.rs new file mode 100644 index 00000000..7ecfd726 --- /dev/null +++ b/spdlog-internal/src/pattern_parser/registry.rs @@ -0,0 +1,91 @@ +use std::{ + borrow::Cow, + collections::{hash_map::Entry, HashMap}, + fmt::Debug, +}; + +use super::{error::TemplateError, BuiltInFormatter, Error, PatternKind, Result}; +use crate::impossible; + +#[derive(Clone, Debug)] +pub struct PatternRegistry { + formatters: HashMap, PatternKind>, +} + +impl PatternRegistry { + pub fn with_builtin() -> Self { + let mut registry = Self { + formatters: HashMap::new(), + }; + + BuiltInFormatter::iter().for_each(|formatter| registry.register_builtin(formatter)); + registry + } + + pub fn register_custom( + &mut self, + placeholder: impl Into>, + factory: F, + ) -> Result<()> { + let placeholder = placeholder.into(); + + let incoming = PatternKind::Custom { + placeholder: placeholder.clone(), + factory, + }; + + match self.formatters.entry(placeholder) { + Entry::Occupied(entry) => Err(Error::ConflictName { + existing: entry.get().to_factory_erased(), + incoming: incoming.to_factory_erased(), + }), + Entry::Vacant(entry) => { + entry.insert(incoming); + Ok(()) + } + } + } + + pub fn find(&self, find_custom: bool, placeholder: impl AsRef) -> Result<&PatternKind> { + let placeholder = placeholder.as_ref(); + + match self.formatters.get(placeholder) { + Some(found) => match (found, find_custom) { + (PatternKind::BuiltIn(_), false) => Ok(found), + (PatternKind::Custom { .. }, true) => Ok(found), + (PatternKind::BuiltIn(_), true) => { + Err(Error::Template(TemplateError::WrongPatternKindReference { + is_builtin_as_custom: true, + placeholder: placeholder.into(), + })) + } + (PatternKind::Custom { .. }, false) => { + Err(Error::Template(TemplateError::WrongPatternKindReference { + is_builtin_as_custom: false, + placeholder: placeholder.into(), + })) + } + }, + None => Err(Error::Template(TemplateError::UnknownPatternReference { + is_custom: find_custom, + placeholder: placeholder.into(), + })), + } + } +} + +impl PatternRegistry { + pub(crate) fn register_builtin(&mut self, formatter: BuiltInFormatter) { + match self + .formatters + .entry(Cow::Borrowed(formatter.placeholder())) + { + Entry::Occupied(_) => { + impossible!("formatter={:?}", formatter) + } + Entry::Vacant(entry) => { + entry.insert(PatternKind::BuiltIn(formatter)); + } + } + } +} diff --git a/spdlog-macros/Cargo.toml b/spdlog-macros/Cargo.toml index ed9d223d..5c84af29 100644 --- a/spdlog-macros/Cargo.toml +++ b/spdlog-macros/Cargo.toml @@ -17,4 +17,6 @@ proc-macro = true nom = "7.1.1" proc-macro2 = "1.0.47" quote = "1.0.21" +self_cell = "1.0.1" +spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal" } syn = { version = "2.0.38", features = ["full"] } diff --git a/spdlog-macros/src/lib.rs b/spdlog-macros/src/lib.rs index c992cb2b..35149b4a 100644 --- a/spdlog-macros/src/lib.rs +++ b/spdlog-macros/src/lib.rs @@ -5,35 +5,15 @@ //! //! [`spdlog-rs`]: https://crates.io/crates/spdlog-rs -mod helper; -mod parse; -mod synthesis; +mod pattern; use proc_macro::TokenStream; -use crate::{ - parse::Pattern, - synthesis::{PatternFormatter, PatternFormatterKind, Synthesiser}, -}; - #[proc_macro] pub fn pattern(input: TokenStream) -> TokenStream { - let pat = syn::parse_macro_input!(input as Pattern); - - let mut synthesiser = Synthesiser::with_builtin_formatters(); - for (name, formatter) in pat.custom_pat_mapping.mapping_pairs { - if let Err(err) = synthesiser.add_formatter_mapping( - name.to_string(), - PatternFormatter { - factory_path: formatter.0, - kind: PatternFormatterKind::Custom, - }, - ) { - panic!("{}", err); - } - } + let pattern = syn::parse_macro_input!(input); - match synthesiser.synthesis(&pat.template) { + match pattern::pattern_impl(pattern) { Ok(stream) => stream.into(), Err(err) => panic!("{}", err), } diff --git a/spdlog-macros/src/parse.rs b/spdlog-macros/src/parse.rs deleted file mode 100644 index 51544fa5..00000000 --- a/spdlog-macros/src/parse.rs +++ /dev/null @@ -1,454 +0,0 @@ -use nom::Parser; -use syn::{ - braced, - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Ident, LitStr, Path, Token, -}; - -use crate::{helper, synthesis::PatternFormatterKind}; - -/// A parsed pattern. -/// -/// A [`Pattern`] gives a structural representation of a pattern parsed from the -/// token stream given to the `pattern` macro. -pub(crate) struct Pattern { - /// The template string included in the pattern. - pub(crate) template: PatternTemplate, - - /// Any user-provided pattern-to-formatter mapping. - pub(crate) custom_pat_mapping: CustomPatternMapping, -} - -impl Parse for Pattern { - fn parse(input: ParseStream) -> syn::Result { - let template_literal: LitStr = input.parse()?; - let template = PatternTemplate::parse_from_template(template_literal)?; - - input.parse::>()?; - let custom_pat_mapping = input.parse()?; - - Ok(Self { - template, - custom_pat_mapping, - }) - } -} - -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub(crate) struct PatternTemplate { - pub(crate) tokens: Vec, -} - -impl PatternTemplate { - fn parse_from_template(template: LitStr) -> syn::Result { - let template_value = template.value(); - let mut parser = Self::parser(); - - let template_str = template_value.as_str(); - let (_, parsed_template) = parser.parse(template_str).map_err(|err| { - let parser_err = match err { - nom::Err::Incomplete(..) => { - // The "complete" combinator should transform `Incomplete` into `Error` - unreachable!(); - } - nom::Err::Error(err) => err, - nom::Err::Failure(err) => err, - }; - let err_byte_position = unsafe { - parser_err - .input - .as_bytes() - .as_ptr() - .offset_from(template_str.as_bytes().as_ptr()) - } as usize; - - let err_span = template - .token() - .subspan(err_byte_position..) - .unwrap_or_else(|| template.span()); - syn::Error::new(err_span, "failed to parse pattern template string") - })?; - - Ok(parsed_template) - } - - #[must_use] - fn parser<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let token_parser = PatternTemplateToken::parser(); - nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof)) - .map(|(tokens, _)| Self { tokens }) - } - - #[must_use] - fn parser_without_style_range<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let token_parser = PatternTemplateToken::parser_without_style_range(); - nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof)) - .map(|(tokens, _)| Self { tokens }) - } -} - -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub(crate) enum PatternTemplateToken { - Literal(PatternTemplateLiteral), - Formatter(PatternTemplateFormatter), - StyleRange(PatternTemplateStyleRange), -} - -impl PatternTemplateToken { - #[must_use] - fn parser<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let style_range_parser = PatternTemplateStyleRange::parser(); - let other_parser = Self::parser_without_style_range(); - - nom::combinator::map(style_range_parser, Self::StyleRange).or(other_parser) - } - - #[must_use] - fn parser_without_style_range<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let literal_parser = PatternTemplateLiteral::parser(); - let formatter_parser = PatternTemplateFormatter::parser(); - - nom::combinator::map(literal_parser, Self::Literal) - .or(nom::combinator::map(formatter_parser, Self::Formatter)) - } -} - -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub(crate) struct PatternTemplateLiteral { - pub(crate) literal: String, -} - -impl PatternTemplateLiteral { - #[must_use] - fn parser<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let literal_char_parser = nom::combinator::value('{', nom::bytes::complete::tag("{{")) - .or(nom::combinator::value('}', nom::bytes::complete::tag("}}"))) - .or(nom::character::complete::none_of("{")); - nom::multi::many1(literal_char_parser).map(|literal_chars| Self { - literal: literal_chars.into_iter().collect(), - }) - } -} - -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub(crate) struct PatternTemplateFormatter { - pub(crate) name: String, - pub(crate) kind: PatternFormatterKind, -} - -impl PatternTemplateFormatter { - #[must_use] - fn parser<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - let open_paren_parser = nom::character::complete::char('{'); - let close_paren_parser = nom::character::complete::char('}'); - let formatter_prefix_parser = nom::character::complete::char('$'); - let formatter_name_parser = nom::combinator::recognize(nom::sequence::tuple(( - nom::combinator::opt(formatter_prefix_parser), - nom::branch::alt(( - nom::character::complete::alpha1, - nom::bytes::complete::tag("_"), - )), - nom::multi::many0_count(nom::branch::alt(( - nom::character::complete::alphanumeric1, - nom::bytes::complete::tag("_"), - ))), - ))); - - nom::sequence::delimited(open_paren_parser, formatter_name_parser, close_paren_parser).map( - |name: &str| match name.strip_prefix('$') { - Some(custom_name) => Self { - name: custom_name.to_owned(), - kind: PatternFormatterKind::Custom, - }, - None => Self { - name: name.to_owned(), - kind: PatternFormatterKind::BuiltIn, - }, - }, - ) - } -} - -#[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub(crate) struct PatternTemplateStyleRange { - pub(crate) body: PatternTemplate, -} - -impl PatternTemplateStyleRange { - #[must_use] - fn parser<'a>() -> impl Parser<&'a str, Self, nom::error::Error<&'a str>> { - nom::bytes::complete::tag("{^") - .and(helper::take_until_unbalanced('{', '}')) - .and(nom::bytes::complete::tag("}")) - .map(|((_, body), _)| body) - .and_then(PatternTemplate::parser_without_style_range()) - .map(|body| Self { body }) - } -} - -/// Mapping from user-provided patterns to formatters. -pub(crate) struct CustomPatternMapping { - pub(crate) mapping_pairs: Vec<(Ident, CustomPatternFactoryFunctionId)>, -} - -impl Parse for CustomPatternMapping { - fn parse(input: ParseStream) -> syn::Result { - let items = Punctuated::::parse_terminated(input)?; - - let mapping_pairs = items.into_iter().fold(vec![], |mut prev, item| { - prev.push((item.name, item.factory)); - prev - }); - - Ok(Self { mapping_pairs }) - } -} - -/// Identifier of a function that produces custom pattern formatters. -#[derive(Clone)] -pub(crate) struct CustomPatternFactoryFunctionId(pub(crate) Path); - -impl From for CustomPatternFactoryFunctionId { - fn from(p: Path) -> Self { - Self(p) - } -} - -impl Parse for CustomPatternFactoryFunctionId { - fn parse(input: ParseStream) -> syn::Result { - let p = input.parse()?; - Ok(Self(p)) - } -} - -struct CustomPatternMappingItem { - name: Ident, - factory: CustomPatternFactoryFunctionId, -} - -impl Parse for CustomPatternMappingItem { - fn parse(input: ParseStream) -> syn::Result { - let name_input; - braced!(name_input in input); - - name_input.parse::()?; - - let name = name_input.parse()?; - input.parse::]>()?; - let factory: CustomPatternFactoryFunctionId = input.parse()?; - - Ok(Self { name, factory }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod template_parsing { - use super::*; - - fn parse_template_str(template: &str) -> nom::IResult<&str, PatternTemplate> { - PatternTemplate::parser().parse(template) - } - - #[test] - fn test_parse_basic() { - assert_eq!( - parse_template_str(r#"hello"#), - Ok(( - "", - PatternTemplate { - tokens: vec![PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello"), - }),], - } - )) - ); - } - - #[test] - fn test_parse_empty() { - assert_eq!( - parse_template_str(""), - Ok(("", PatternTemplate { tokens: Vec::new() },)) - ); - } - - #[test] - fn test_parse_escape_literal() { - assert_eq!( - parse_template_str(r#"hello {{name}}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello {name}"), - }),], - } - )) - ); - } - - #[test] - fn test_parse_escape_literal_at_beginning() { - assert_eq!( - parse_template_str(r#"{{name}}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("{name}"), - }),], - } - )) - ); - } - - #[test] - fn test_parse_formatter_basic() { - assert_eq!( - parse_template_str(r#"hello {name}!{$custom}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![ - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello "), - }), - PatternTemplateToken::Formatter(PatternTemplateFormatter { - name: String::from("name"), - kind: PatternFormatterKind::BuiltIn - }), - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("!"), - }), - PatternTemplateToken::Formatter(PatternTemplateFormatter { - name: String::from("custom"), - kind: PatternFormatterKind::Custom - }), - ], - } - )) - ); - } - - #[test] - fn test_parse_literal_single_close_paren() { - assert_eq!( - parse_template_str(r#"hello name}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello name}"), - }),], - } - )) - ); - } - - #[test] - fn test_parse_formatter_invalid_name() { - assert!(parse_template_str(r#"hello {name{}!"#).is_err()); - } - - #[test] - fn test_parse_formatter_missing_close_paren() { - assert!(parse_template_str(r#"hello {name"#).is_err()); - } - - #[test] - fn test_parse_formatter_duplicate_close_paren() { - assert_eq!( - parse_template_str(r#"hello {name}}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![ - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello "), - }), - PatternTemplateToken::Formatter(PatternTemplateFormatter { - name: String::from("name"), - kind: PatternFormatterKind::BuiltIn - }), - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("}"), - }), - ], - } - )) - ); - } - - #[test] - fn test_parse_style_range_basic() { - assert_eq!( - parse_template_str(r#"hello {^world}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![ - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello "), - }), - PatternTemplateToken::StyleRange(PatternTemplateStyleRange { - body: PatternTemplate { - tokens: vec![PatternTemplateToken::Literal( - PatternTemplateLiteral { - literal: String::from("world"), - } - ),], - }, - }), - ], - } - )) - ); - - assert_eq!( - parse_template_str(r#"hello {^world {b_pat} {$c_pat} {{escape}}}"#), - Ok(( - "", - PatternTemplate { - tokens: vec![ - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("hello "), - }), - PatternTemplateToken::StyleRange(PatternTemplateStyleRange { - body: PatternTemplate { - tokens: vec![ - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from("world "), - }), - PatternTemplateToken::Formatter(PatternTemplateFormatter { - name: String::from("b_pat"), - kind: PatternFormatterKind::BuiltIn - }), - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from(" "), - }), - PatternTemplateToken::Formatter(PatternTemplateFormatter { - name: String::from("c_pat"), - kind: PatternFormatterKind::Custom - }), - PatternTemplateToken::Literal(PatternTemplateLiteral { - literal: String::from(" {escape}"), - }), - ], - }, - }), - ], - } - )) - ); - } - - #[test] - fn test_parse_style_range_nested() { - assert!(parse_template_str(r#"hello {^ hello {^ world } }"#).is_err()); - } - } -} diff --git a/spdlog-macros/src/pattern/mod.rs b/spdlog-macros/src/pattern/mod.rs new file mode 100644 index 00000000..472813e6 --- /dev/null +++ b/spdlog-macros/src/pattern/mod.rs @@ -0,0 +1,109 @@ +mod synthesis; + +use proc_macro2::TokenStream; +use self_cell::self_cell; +use spdlog_internal::pattern_parser::{parse::Template, PatternRegistry, Result}; +use syn::{ + braced, + parse::{Parse, ParseStream}, + Ident, LitStr, Path, Token, +}; +use synthesis::Synthesiser; + +pub fn pattern_impl(pattern: Pattern) -> Result { + let mut registry = PatternRegistry::with_builtin(); + for (name, formatter) in pattern.custom_patterns() { + registry.register_custom(name.to_string(), formatter.clone())?; + } + + Synthesiser::new(registry).synthesize(pattern.template()) +} + +/// A parsed pattern. +/// +/// A [`Pattern`] gives a structural representation of a pattern parsed from the +/// token stream given to the `pattern` macro. + +pub struct Pattern { + /// The template string included in the pattern. + template: TemplateSelfRef, + /// Any user-provided pattern-to-formatter mapping. + custom_patterns: CustomPatterns, +} + +self_cell! { + pub struct TemplateSelfRef { + owner: String, + #[covariant] + dependent: Template, + } +} + +impl Pattern { + fn custom_patterns(&self) -> impl IntoIterator { + self.custom_patterns.0.iter() + } + + fn template(&self) -> &Template { + self.template.borrow_dependent() + } +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> syn::Result { + let template_lit = input.parse::()?; + input.parse::>()?; + let custom_patterns = input.parse()?; + + let ret = Pattern { + template: TemplateSelfRef::try_new(template_lit.value(), |template_str| { + Template::parse(template_str).map_err(|err| { + syn::Error::new( + // TODO: Maybe we can make a subspan for the literal for a better error + // message + template_lit.span(), + err, + ) + }) + })?, + custom_patterns, + }; + Ok(ret) + } +} + +///// + +/// Mapping from user-provided patterns to formatters. +struct CustomPatterns(Vec<(Ident, Path)>); + +impl Parse for CustomPatterns { + fn parse(input: ParseStream) -> syn::Result { + let items = input.parse_terminated(CustomPatternItem::parse, Token![,])?; + + let mapping_pairs = items + .into_iter() + .map(|item| (item.name, item.factory)) + .collect(); + + Ok(Self(mapping_pairs)) + } +} + +struct CustomPatternItem { + name: Ident, + factory: Path, +} + +impl Parse for CustomPatternItem { + fn parse(input: ParseStream) -> syn::Result { + let name_input; + braced!(name_input in input); + name_input.parse::()?; + let name = name_input.parse()?; + input.parse::]>()?; + let factory = input.parse()?; + + Ok(Self { name, factory }) + } +} diff --git a/spdlog-macros/src/pattern/synthesis.rs b/spdlog-macros/src/pattern/synthesis.rs new file mode 100644 index 00000000..e2e147b6 --- /dev/null +++ b/spdlog-macros/src/pattern/synthesis.rs @@ -0,0 +1,96 @@ +use std::borrow::Cow; + +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use spdlog_internal::pattern_parser::{ + error::TemplateError, + parse::{Template, TemplateFormatterToken, TemplateLiteral, TemplateToken}, + Error, PatternKind as GenericPatternKind, PatternRegistry as GenericPatternRegistry, Result, +}; +use syn::{Expr, ExprLit, Lit, LitStr, Path}; + +type PatternRegistry = GenericPatternRegistry; +type PatternKind = GenericPatternKind; + +pub(crate) struct Synthesiser { + registry: PatternRegistry, +} + +impl Synthesiser { + pub fn new(registry: PatternRegistry) -> Self { + Self { registry } + } + + pub fn synthesize(&self, template: &Template) -> Result { + let expr = self.build_expr(template, false)?; + Ok(expr.into_token_stream()) + } + + fn build_expr(&self, template: &Template, mut style_range_seen: bool) -> Result { + let mut tuple_elems = Vec::with_capacity(template.tokens.len()); + + for token in &template.tokens { + let token_template_expr = match token { + TemplateToken::Literal(literal_token) => self.build_literal(literal_token)?, + TemplateToken::Formatter(formatter_token) => { + self.build_formatter_creation(formatter_token)? + } + TemplateToken::StyleRange(style_range_token) => { + if style_range_seen { + return Err(Error::Template(TemplateError::MultipleStyleRange)); + } + style_range_seen = true; + let nested_pattern = self.build_expr(&style_range_token.body, true)?; + self.build_style_range_creation(nested_pattern)? + } + }; + tuple_elems.push(token_template_expr); + } + + let stream = quote::quote! { ( #(#tuple_elems ,)* ) }; + let expr = syn::parse2(stream).unwrap(); + Ok(Expr::Tuple(expr)) + } + + fn build_literal(&self, literal_token: &TemplateLiteral) -> Result { + let lit = LitStr::new(&literal_token.literal, Span::mixed_site()); + let expr = Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: Lit::Str(lit), + }); + Ok(expr) + } + + fn build_formatter_creation(&self, formatter_token: &TemplateFormatterToken) -> Result { + let pattern = self.registry.find( + formatter_token.has_custom_prefix, + formatter_token.placeholder, + )?; + + let factory = factory_of_pattern(pattern); + let stream = quote::quote!( #factory() ); + let factory_call = syn::parse2(stream).unwrap(); + Ok(Expr::Call(factory_call)) + } + + fn build_style_range_creation(&self, body: Expr) -> Result { + let style_range_pattern_new_path: Path = + syn::parse_str("::spdlog::formatter::__pattern::StyleRange::new").unwrap(); + let stream = quote::quote!( #style_range_pattern_new_path (#body) ); + let expr = syn::parse2(stream).unwrap(); + Ok(Expr::Call(expr)) + } +} + +pub(crate) fn factory_of_pattern(pattern: &PatternKind) -> Cow { + match pattern { + PatternKind::BuiltIn(builtin) => Cow::Owned( + syn::parse_str::(&format!( + "::spdlog::formatter::__pattern::{}::default", + builtin.struct_name() + )) + .unwrap(), + ), + PatternKind::Custom { factory, .. } => Cow::Borrowed(factory), + } +} diff --git a/spdlog-macros/src/synthesis.rs b/spdlog-macros/src/synthesis.rs deleted file mode 100644 index c9e26a46..00000000 --- a/spdlog-macros/src/synthesis.rs +++ /dev/null @@ -1,301 +0,0 @@ -use std::{ - collections::HashMap, - error::Error, - fmt::{Display, Formatter}, -}; - -use proc_macro2::{Span, TokenStream}; -use quote::ToTokens; -use syn::{Expr, ExprLit, Lit, LitStr, Path}; - -use crate::parse::{ - PatternTemplate, PatternTemplateFormatter, PatternTemplateLiteral, PatternTemplateStyleRange, - PatternTemplateToken, -}; - -pub(crate) struct Synthesiser { - formatters: HashMap, -} - -impl Synthesiser { - #[must_use] - pub(crate) fn new() -> Self { - Self { - formatters: HashMap::new(), - } - } - - #[must_use] - pub(crate) fn with_builtin_formatters() -> Self { - let mut synthesiser = Self::new(); - - macro_rules! map_builtin_formatters { - ( - $synthesiser:expr, - $( [ $($name:literal),+ $(,)? ] => $formatter:ident ),+ - $(,)? - ) => { - $( - $( - // All built-in patterns implement the `Default` trait. So we use the - // `default` function to create instances of the built-in patterns. - $synthesiser.add_formatter_mapping( - String::from($name), - PatternFormatter { - factory_path: syn::parse_str( - stringify!(::spdlog::formatter::__pattern::$formatter::default) - ).unwrap(), - kind: PatternFormatterKind::BuiltIn, - } - ).unwrap(); - )+ - )+ - }; - } - - map_builtin_formatters! {synthesiser, - ["weekday_name"] => AbbrWeekdayName, - ["weekday_name_full"] => WeekdayName, - ["month_name"] => AbbrMonthName, - ["month_name_full"] => MonthName, - ["datetime"] => FullDateTime, - ["year_short"] => ShortYear, - ["year"] => Year, - ["date_short"] => ShortDate, - ["date"] => Date, - ["month"] => Month, - ["day"] => Day, - ["hour"] => Hour, - ["hour_12"] => Hour12, - ["minute"] => Minute, - ["second"] => Second, - ["millisecond"] => Millisecond, - ["microsecond"] => Microsecond, - ["nanosecond"] => Nanosecond, - ["am_pm"] => AmPm, - ["time_12"] => Time12, - ["time_short"] => ShortTime, - ["time"] => Time, - ["tz_offset"] => TzOffset, - ["unix_timestamp"] => UnixTimestamp, - ["full"] => Full, - ["level"] => Level, - ["level_short"] => ShortLevel, - ["source"] => Source, - ["file_name"] => SourceFilename, - ["file"] => SourceFile, - ["line"] => SourceLine, - ["column"] => SourceColumn, - ["module_path"] => SourceModulePath, - ["logger"] => LoggerName, - ["payload"] => Payload, - ["pid"] => ProcessId, - ["tid"] => ThreadId, - ["eol"] => Eol, - } - - synthesiser - } - - pub(crate) fn add_formatter_mapping( - &mut self, - name: String, - formatter: PatternFormatter, - ) -> Result<(), ConflictFormatterError> { - if let Some(conflicted) = self.formatters.get(&name) { - return Err(ConflictFormatterError { - name, - with: (formatter.kind, conflicted.kind), - }); - } - - self.formatters.insert(name, formatter); - Ok(()) - } - - pub(crate) fn synthesis( - &self, - template: &PatternTemplate, - ) -> Result { - let expr = self.build_template_pattern_expr(template, false)?; - Ok(expr.into_token_stream()) - } - - fn build_template_pattern_expr( - &self, - template: &PatternTemplate, - mut style_range_seen: bool, - ) -> Result { - let mut tuple_elems = Vec::with_capacity(template.tokens.len()); - for token in &template.tokens { - let token_template_expr = match token { - PatternTemplateToken::Literal(literal_token) => { - self.build_literal_template_pattern_expr(literal_token)? - } - PatternTemplateToken::Formatter(formatter_token) => { - self.build_formatter_template_pattern_expr(formatter_token)? - } - PatternTemplateToken::StyleRange(style_range_token) => { - if style_range_seen { - return Err(SynthesisError::MultipleStyleRange); - } - style_range_seen = true; - self.build_style_range_template_pattern_expr(style_range_token)? - } - }; - tuple_elems.push(token_template_expr); - } - - let stream = quote::quote! { ( #(#tuple_elems ,)* ) }; - let expr = syn::parse2(stream).unwrap(); - Ok(Expr::Tuple(expr)) - } - - fn build_literal_template_pattern_expr( - &self, - literal_token: &PatternTemplateLiteral, - ) -> Result { - let lit = LitStr::new(&literal_token.literal, Span::mixed_site()); - let expr = Expr::Lit(ExprLit { - attrs: Vec::new(), - lit: Lit::Str(lit), - }); - Ok(expr) - } - - fn build_formatter_template_pattern_expr( - &self, - formatter_token: &PatternTemplateFormatter, - ) -> Result { - let formatter_creation_expr = self.build_formatter_creation_expr(formatter_token)?; - Ok(formatter_creation_expr) - } - - fn build_style_range_template_pattern_expr( - &self, - style_range_token: &PatternTemplateStyleRange, - ) -> Result { - let body_pattern_expr = self.build_template_pattern_expr(&style_range_token.body, true)?; - let expr = self.build_style_range_pattern_creation_expr(body_pattern_expr)?; - Ok(expr) - } - - fn build_formatter_creation_expr( - &self, - formatter_token: &PatternTemplateFormatter, - ) -> Result { - let formatter = match self.formatters.get(&formatter_token.name) { - Some(formatter) => { - if formatter_token.kind == formatter.kind { - Ok(formatter) - } else { - Err(SynthesisError::BuiltinPatternUsedAsCustomPattern( - formatter_token.name.clone(), - )) - } - } - None => Err(SynthesisError::UnknownFormatterName( - formatter_token.name.clone(), - formatter_token.kind, - )), - }?; - let formatter_factory_path = &formatter.factory_path; - - let stream = quote::quote!( #formatter_factory_path () ); - let factory_call_expr = syn::parse2(stream).unwrap(); - Ok(Expr::Call(factory_call_expr)) - } - - fn build_style_range_pattern_creation_expr(&self, body: Expr) -> Result { - let style_range_pattern_new_path: Path = - syn::parse_str("::spdlog::formatter::__pattern::StyleRange::new").unwrap(); - let stream = quote::quote!( #style_range_pattern_new_path (#body) ); - let expr = syn::parse2(stream).unwrap(); - Ok(Expr::Call(expr)) - } -} - -pub(crate) struct PatternFormatter { - pub(crate) factory_path: Path, - pub(crate) kind: PatternFormatterKind, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum PatternFormatterKind { - Custom, - BuiltIn, -} - -#[derive(Debug)] -pub(crate) struct ConflictFormatterError { - name: String, - with: (PatternFormatterKind, PatternFormatterKind), -} - -impl Display for ConflictFormatterError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use PatternFormatterKind as Kind; - - match self.with { - (Kind::Custom, Kind::BuiltIn) => { - write!( - f, - "'{}' is already a built-in pattern, please try another name", - self.name - ) - } - (Kind::Custom, Kind::Custom) => { - write!( - f, - "the constructor of custom pattern '{}' is specified more than once", - self.name - ) - } - (Kind::BuiltIn, _) => { - write!( - f, - "this should not happen, please open an issue on 'spdlog-rs' Bug Tracker. debug: {:?}", - self - ) - } - } - } -} - -impl Error for ConflictFormatterError {} - -#[derive(Debug)] -pub(crate) enum SynthesisError { - BuiltinPatternUsedAsCustomPattern(String), - UnknownFormatterName(String, PatternFormatterKind), - MultipleStyleRange, -} - -impl Display for SynthesisError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use PatternFormatterKind as Kind; - - match self { - Self::BuiltinPatternUsedAsCustomPattern(name) => { - write!( - f, - "'{}' is a built-in pattern, it cannot be used as a custom pattern. try to replace it with `{{{}}}`", - name, name - ) - } - Self::UnknownFormatterName(name, kind) => match kind { - Kind::BuiltIn => write!(f, "no built-in pattern named '{}'", name), - Kind::Custom => write!( - f, - "the constructor of custom pattern '{}' is not specified", - name - ), - }, - Self::MultipleStyleRange => { - write!(f, "multiple style ranges are not currently supported") - } - } - } -} - -impl Error for SynthesisError {} From 3f145dbc3337e14e648be075b3f89a6e010145ea Mon Sep 17 00:00:00 2001 From: Asuna Date: Thu, 26 Oct 2023 04:09:13 +0800 Subject: [PATCH 2/4] Implement `RuntimePattern` --- .github/workflows/ci.yml | 2 +- spdlog/Cargo.toml | 4 + spdlog/benches/pattern.rs | 268 ++++------- spdlog/src/error.rs | 34 ++ spdlog/src/formatter/pattern_formatter/mod.rs | 122 ++--- .../pattern_formatter/pattern/style_range.rs | 2 +- .../formatter/pattern_formatter/runtime.rs | 418 ++++++++++++++++++ spdlog/src/lib.rs | 45 +- spdlog/tests/pattern.rs | 190 ++++---- 9 files changed, 744 insertions(+), 341 deletions(-) create mode 100644 spdlog/src/formatter/pattern_formatter/runtime.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f5450f0..7b480531 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] - fn_features: ['', 'log native libsystemd multi-thread'] + fn_features: ['', 'log native libsystemd multi-thread runtime-pattern'] cfg_feature: ['', 'flexible-string', 'source-location'] runs-on: ${{ matrix.os }} steps: diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 79c01742..f26b092c 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -36,6 +36,7 @@ source-location = [] native = [] libsystemd = ["libsystemd-sys"] multi-thread = ["crossbeam"] +runtime-pattern = ["spdlog-internal"] [dependencies] arc-swap = "1.5.1" @@ -43,11 +44,13 @@ atomic = "0.5.1" cfg-if = "1.0.0" chrono = "0.4.22" crossbeam = { version = "0.8.2", optional = true } +dyn-clone = "1.0.14" flexible-string = { version = "0.1.0", optional = true } if_chain = "1.0.2" is-terminal = "0.4" log = { version = "0.4.8", optional = true } once_cell = "1.16.0" +spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true } spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" } spin = "0.9.8" thiserror = "1.0.37" @@ -76,6 +79,7 @@ flexi_logger = "=0.24.1" tracing = "=0.1.37" tracing-subscriber = "=0.3.16" tracing-appender = "=0.2.2" +paste = "1.0.14" [build-dependencies] rustc_version = "0.4.0" diff --git a/spdlog/benches/pattern.rs b/spdlog/benches/pattern.rs index f5b1ea05..9f6fb552 100644 --- a/spdlog/benches/pattern.rs +++ b/spdlog/benches/pattern.rs @@ -4,8 +4,9 @@ extern crate test; use std::{cell::RefCell, sync::Arc}; +use paste::paste; use spdlog::{ - formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter}, + formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter, RuntimePattern}, prelude::*, sink::Sink, Record, StringBuf, @@ -76,15 +77,7 @@ fn bench_pattern(bencher: &mut Bencher, pattern: impl Pattern + Clone + 'static) bench_formatter(bencher, PatternFormatter::new(pattern)); } -#[bench] -fn bench_1_full_formatter(bencher: &mut Bencher) { - bench_formatter(bencher, FullFormatter::new()) -} - -#[bench] -fn bench_2_full_pattern(bencher: &mut Bencher) { - let pattern = pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"); - +fn bench_full_pattern(bencher: &mut Bencher, pattern: impl Pattern + Clone + 'static) { let full_formatter = Arc::new(StringSink::with(|b| { b.formatter(Box::new(FullFormatter::new())) })); @@ -103,192 +96,81 @@ fn bench_2_full_pattern(bencher: &mut Bencher) { bench_pattern(bencher, pattern) } -#[bench] -fn bench_weekday_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{weekday_name}")) -} - -#[bench] -fn bench_weekday_name_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{weekday_name_full}")) -} - -#[bench] -fn bench_month_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month_name}")) -} - -#[bench] -fn bench_month_name_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month_name_full}")) -} - -#[bench] -fn bench_datetime(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{datetime}")) -} - -#[bench] -fn bench_year_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{year_short}")) -} - -#[bench] -fn bench_year(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{year}")) -} - -#[bench] -fn bench_date_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{date_short}")) -} - -#[bench] -fn bench_date(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{date}")) -} - -#[bench] -fn bench_month(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{month}")) -} - -#[bench] -fn bench_day(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{day}")) -} - -#[bench] -fn bench_hour(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{hour}")) -} - -#[bench] -fn bench_hour_12(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{hour_12}")) -} - -#[bench] -fn bench_minute(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{minute}")) -} - -#[bench] -fn bench_second(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{second}")) -} - -#[bench] -fn bench_millsecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{millisecond}")) -} - -#[bench] -fn bench_microsecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{microsecond}")) -} - -#[bench] -fn bench_nanosecond(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{nanosecond}")) -} - -#[bench] -fn bench_am_pm(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{am_pm}")) -} - -#[bench] -fn bench_time_12(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time_12}")) -} - -#[bench] -fn bench_time_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time_short}")) -} - -#[bench] -fn bench_time(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{time}")) -} - -#[bench] -fn bench_tz_offset(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{tz_offset}")) -} - -#[bench] -fn bench_unix_timestamp(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{unix_timestamp}")) -} - -#[bench] -fn bench_full(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{full}")) -} - -#[bench] -fn bench_level(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{level}")) -} - -#[bench] -fn bench_level_short(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{level_short}")) -} +// #[bench] -fn bench_source(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{source}")) -} - -#[bench] -fn bench_file_name(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{file_name}")) -} - -#[bench] -fn bench_file(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{file}")) -} - -#[bench] -fn bench_line(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{line}")) -} - -#[bench] -fn bench_column(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{column}")) -} - -#[bench] -fn bench_module_path(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{module_path}")) -} - -#[bench] -fn bench_logger(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{logger}")) -} - -#[bench] -fn bench_payload(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{payload}")) -} - -#[bench] -fn bench_pid(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{pid}")) -} - -#[bench] -fn bench_tid(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{tid}")) +fn bench_1_full_formatter(bencher: &mut Bencher) { + bench_formatter(bencher, FullFormatter::new()) } #[bench] -fn bench_eol(bencher: &mut Bencher) { - bench_pattern(bencher, pattern!("{eol}")) +fn bench_2_full_pattern_ct(bencher: &mut Bencher) { + bench_full_pattern( + bencher, + pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"), + ) +} + +#[bench] +fn bench_3_full_pattern_rt(bencher: &mut Bencher) { + bench_full_pattern( + bencher, + RuntimePattern::new("[{date} {time}.{millisecond}] [{level}] {payload}{eol}").unwrap(), + ) +} + +macro_rules! bench_patterns { + ( $(($name:ident, $placeholder:literal)),+ $(,)? ) => { + $(paste! { + #[bench] + fn [](bencher: &mut Bencher) { + bench_pattern(bencher, pattern!($placeholder)) + } + #[bench] + fn [](bencher: &mut Bencher) { + bench_pattern(bencher, RuntimePattern::new($placeholder).unwrap()) + } + })+ + }; +} + +bench_patterns! { + (weekday_name, "{weekday_name}"), + (weekday_name_full, "{weekday_name_full}"), + (month_name, "{month_name}"), + (month_name_full, "{month_name_full}"), + (datetime, "{datetime}"), + (year_short, "{year_short}"), + (year, "{year}"), + (date_short, "{date_short}"), + (date, "{date}"), + (month, "{month}"), + (day, "{day}"), + (hour, "{hour}"), + (hour_12, "{hour_12}"), + (minute, "{minute}"), + (second, "{second}"), + (millsecond, "{millisecond}"), + (microsecond, "{microsecond}"), + (nanosecond, "{nanosecond}"), + (am_pm, "{am_pm}"), + (time_12, "{time_12}"), + (time_short, "{time_short}"), + (time, "{time}"), + (tz_offset, "{tz_offset}"), + (unix_timestamp, "{unix_timestamp}"), + (full, "{full}"), + (level, "{level}"), + (level_short, "{level_short}"), + (source, "{source}"), + (file_name, "{file_name}"), + (file, "{file}"), + (line, "{line}"), + (column, "{column}"), + (module_path, "{module_path}"), + (logger, "{logger}"), + (payload, "{payload}"), + (pid, "{pid}"), + (tid, "{tid}"), + (eol, "{eol}"), } diff --git a/spdlog/src/error.rs b/spdlog/src/error.rs index 41316dd0..6deccd0f 100644 --- a/spdlog/src/error.rs +++ b/spdlog/src/error.rs @@ -92,6 +92,14 @@ pub enum Error { #[error("failed to send message to channel: {0}")] SendToChannel(SendToChannelError, SendToChannelErrorDropped), + /// The variant returned by [`RuntimePatternBuilder::build`] when the + /// pattern is failed to be built at runtime. + /// + /// [`RuntimePatternBuilder::build`]: crate::formatter::RuntimePatternBuilder::build + #[cfg(feature = "runtime-pattern")] + #[error("failed to build pattern at runtime: {0}")] + BuildPattern(BuildPatternError), + /// This variant returned when multiple errors occurred. #[error("{0:?}")] Multiple(Vec), @@ -127,6 +135,16 @@ pub enum InvalidArgumentError { ThreadPoolCapacity(String), } +#[cfg(feature = "runtime-pattern")] +impl Error { + pub(crate) fn err_build_pattern(err: BuildPatternErrorInner) -> Self { + Self::BuildPattern(BuildPatternError(err)) + } + pub(crate) fn err_build_pattern_internal(err: spdlog_internal::pattern_parser::Error) -> Self { + Self::BuildPattern(BuildPatternError(BuildPatternErrorInner::Internal(err))) + } +} + /// This error indicates that an invalid logger name was set. /// /// See the documentation of [`LoggerBuilder::name`] for the name requirements. @@ -242,6 +260,22 @@ impl SendToChannelErrorDropped { } } +/// This error indicates that an error occurred while building a pattern at +/// compile-time. +#[cfg(feature = "runtime-pattern")] +#[derive(Error, Debug)] +#[error("{0}")] +pub struct BuildPatternError(BuildPatternErrorInner); + +#[cfg(feature = "runtime-pattern")] +#[derive(Error, Debug)] +pub(crate) enum BuildPatternErrorInner { + #[error("{0}")] + Internal(spdlog_internal::pattern_parser::Error), + #[error("invalid placeholder for custom pattern '{0}'")] + InvalidCustomPlaceholder(String), +} + /// The result type of this crate. pub type Result = result::Result; diff --git a/spdlog/src/formatter/pattern_formatter/mod.rs b/spdlog/src/formatter/pattern_formatter/mod.rs index baf7aeb0..3221d14d 100644 --- a/spdlog/src/formatter/pattern_formatter/mod.rs +++ b/spdlog/src/formatter/pattern_formatter/mod.rs @@ -14,8 +14,15 @@ #[path = "pattern/mod.rs"] pub mod __pattern; +#[cfg(feature = "runtime-pattern")] +mod runtime; + use std::{fmt::Write, ops::Range, sync::Arc}; +use dyn_clone::*; +#[cfg(feature = "runtime-pattern")] +pub use runtime::*; + use crate::{ formatter::{FmtExtraInfo, FmtExtraInfoBuilder, Formatter}, Error, Record, StringBuf, @@ -41,10 +48,14 @@ use crate::{ /// # #[derive(Default)] /// # struct MyPattern; /// pattern!("text"); -/// pattern!("current line: {line}"); -/// pattern!("custom: {$my_pattern}", {$my_pattern} => MyPattern::default); +/// pattern!("current line: {line}{eol}"); +/// pattern!("custom: {$my_pattern}{eol}", {$my_pattern} => MyPattern::default); /// ``` /// +/// Its first argument accepts only a literal string that is known at compile-time. +/// If you want to build a pattern from a runtime string, use +/// [`RuntimePattern`] instead. +/// /// # Note /// /// The value returned by this macro is implementation details and users should @@ -53,12 +64,12 @@ use crate::{ /// /// # Basic Usage /// -/// In its simplest form, `pattern` receives a **literal** pattern string and +/// In its simplest form, `pattern` receives a **literal** template string and /// converts it into a zero-cost pattern: /// ``` /// use spdlog::formatter::{pattern, PatternFormatter}; /// -/// let formatter = PatternFormatter::new(pattern!("pattern string")); +/// let formatter = PatternFormatter::new(pattern!("template string")); /// ``` /// /// # Using Built-in Patterns @@ -73,7 +84,7 @@ use crate::{ /// use spdlog::info; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] /// -/// let formatter = PatternFormatter::new(pattern!("[{level}] {payload}")); +/// let formatter = PatternFormatter::new(pattern!("[{level}] {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -81,8 +92,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" /// # ); /// ``` /// @@ -98,7 +109,7 @@ use crate::{ /// # info, /// # }; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] -/// let formatter = PatternFormatter::new(pattern!("[{{escaped}}] {payload}")); +/// let formatter = PatternFormatter::new(pattern!("[{{escaped}}] {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -106,8 +117,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[{escaped}] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[{escaped}] Interesting log message\n" /// # ); /// ``` /// @@ -127,7 +138,7 @@ use crate::{ /// # info, /// # }; #[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] -/// let formatter = PatternFormatter::new(pattern!("{^[{level}]} {payload}")); +/// let formatter = PatternFormatter::new(pattern!("{^[{level}]} {payload}{eol}")); /// # let (doctest, sink) = test_utils::echo_logger_from_formatter( /// # Box::new(formatter), /// # None @@ -135,8 +146,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" /// // ^^^^^^ <- style range /// # ); /// ``` @@ -161,17 +172,12 @@ use crate::{ /// struct MyPattern; /// /// impl Pattern for MyPattern { -/// fn format( -/// &self, -/// record: &Record, -/// dest: &mut StringBuf, -/// _ctx: &mut PatternContext, -/// ) -> spdlog::Result<()> { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { /// write!(dest, "My own pattern").map_err(spdlog::Error::FormatRecord) /// } /// } /// -/// let pat = pattern!("[{level}] {payload} - {$mypat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat}{eol}", /// {$mypat} => MyPattern::default, /// ); /// let formatter = PatternFormatter::new(pat); @@ -182,8 +188,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message - My own pattern" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - My own pattern\n" /// # ); /// ``` /// @@ -230,17 +236,12 @@ use crate::{ /// } /// /// impl Pattern for MyPattern { -/// fn format( -/// &self, -/// record: &Record, -/// dest: &mut StringBuf, -/// _ctx: &mut PatternContext, -/// ) -> spdlog::Result<()> { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { /// write!(dest, "{}", self.id).map_err(spdlog::Error::FormatRecord) /// } /// } /// -/// let pat = pattern!("[{level}] {payload} - {$mypat} {$mypat} {$mypat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat} {$mypat} {$mypat}{eol}", /// {$mypat} => MyPattern::new, /// ); /// let formatter = PatternFormatter::new(pat); @@ -251,8 +252,8 @@ use crate::{ /// /// info!(logger: doctest, "Interesting log message"); /// # assert_eq!( -/// # sink.clone_string(), -/// /* Output */ "[info] Interesting log message - 0 1 2" +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - 0 1 2\n" /// # ); /// ``` /// @@ -265,7 +266,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyOtherPattern; /// # -/// let pat = pattern!("[{level}] {payload} - {$mypat} {$myotherpat}", +/// let pat = pattern!("[{level}] {payload} - {$mypat} {$myotherpat}{eol}", /// {$mypat} => MyPattern::default, /// {$myotherpat} => MyOtherPattern::default, /// ); @@ -284,7 +285,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyOtherPattern; /// # -/// let pattern = pattern!("[{level}] {payload} - {$mypat}", +/// let pattern = pattern!("[{level}] {payload} - {$mypat}{eol}", /// {$mypat} => MyPattern::new, /// // Error: name conflicts with another custom pattern /// {$mypat} => MyOtherPattern::new, @@ -297,7 +298,7 @@ use crate::{ /// # #[derive(Default)] /// # struct MyPattern; /// # -/// let pattern = pattern!("[{level}] {payload} - {$day}", +/// let pattern = pattern!("[{level}] {payload} - {$day}{eol}", /// // Error: name conflicts with a built-in pattern /// {$day} => MyPattern::new, /// ); @@ -349,6 +350,7 @@ use crate::{ /// [^1]: Patterns related to source location require that feature /// `source-location` is enabled, otherwise the output is empty. /// +/// [`RuntimePattern`]: crate::formatter::RuntimePattern /// [`FullFormatter`]: crate::formatter::FullFormatter pub use ::spdlog_macros::pattern; @@ -364,8 +366,10 @@ where { /// Creates a new `PatternFormatter` object with the given pattern. /// - /// Currently users can only create a `pattern` object at compile-time by - /// calling [`pattern!`] macro. + /// Currently users can only create a `pattern` object by using: + /// + /// - Macro [`pattern!`] to build a pattern at compile-time. + /// - Struct [`RuntimePattern::builder`] to build a pattern at runtime. #[must_use] pub fn new(pattern: P) -> Self { Self { pattern } @@ -428,7 +432,7 @@ impl PatternContext { /// There are 2 approaches to create your own pattern: /// - Define a new type and implements this trait; /// - Use the [`pattern`] macro to create a pattern from a template string. -pub trait Pattern: Send + Sync { +pub trait Pattern: Send + Sync + DynClone { /// Format this pattern against the given log record and write the formatted /// message into the output buffer. /// @@ -441,6 +445,7 @@ pub trait Pattern: Send + Sync { ctx: &mut PatternContext, ) -> crate::Result<()>; } +clone_trait_object!(Pattern); impl Pattern for String { fn format( @@ -478,23 +483,10 @@ where } } -impl<'a, T> Pattern for &'a mut T -where - T: ?Sized + Pattern, -{ - fn format( - &self, - record: &Record, - dest: &mut StringBuf, - ctx: &mut PatternContext, - ) -> crate::Result<()> { - ::format(*self, record, dest, ctx) - } -} - impl Pattern for Box where T: ?Sized + Pattern, + Self: Clone, { fn format( &self, @@ -509,6 +501,7 @@ where impl Pattern for Arc where T: ?Sized + Pattern, + Self: Clone, { fn format( &self, @@ -520,9 +513,10 @@ where } } -impl Pattern for [T] +impl Pattern for &[T] where T: Pattern, + Self: Clone, { fn format( &self, @@ -530,7 +524,7 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - for p in self { + for p in *self { ::format(p, record, dest, ctx)?; } Ok(()) @@ -540,6 +534,7 @@ where impl Pattern for [T; N] where T: Pattern, + Self: Clone, { fn format( &self, @@ -547,13 +542,17 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - <[T] as Pattern>::format(self, record, dest, ctx) + for p in self { + ::format(p, record, dest, ctx)?; + } + Ok(()) } } impl Pattern for Vec where T: Pattern, + Self: Clone, { fn format( &self, @@ -561,7 +560,10 @@ where dest: &mut StringBuf, ctx: &mut PatternContext, ) -> crate::Result<()> { - <[T] as Pattern>::format(self, record, dest, ctx) + for p in self { + ::format(p, record, dest, ctx)?; + } + Ok(()) } } @@ -585,6 +587,7 @@ macro_rules! tuple_pattern { where $($T : Pattern,)+ last!($($T,)+) : ?Sized, + Self: Clone, { fn format(&self, record: &Record, dest: &mut StringBuf, ctx: &mut PatternContext) -> crate::Result<()> { $( @@ -1256,9 +1259,12 @@ pub mod tests { #[test] fn test_pattern_mut_as_pattern() { - #[allow(unknown_lints)] - #[allow(clippy::needless_borrow, clippy::needless_borrows_for_generic_args)] - test_pattern(&mut String::from("literal"), "literal", None); + // Since we now require `T: Pattern` to implement `Clone`, there is no way to + // accept an `&mut T` as a `Pattern` anymore, since `&mut T` is not cloneable. + // + // test_pattern(&mut String::from("literal"), "literal", None); + #[allow(clippy::deref_addrof)] + test_pattern(&*&mut String::from("literal"), "literal", None); } #[test] diff --git a/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs b/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs index 1183cde8..82c26d79 100644 --- a/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs +++ b/spdlog/src/formatter/pattern_formatter/pattern/style_range.rs @@ -23,7 +23,7 @@ where impl

Pattern for StyleRange

where - P: Pattern, + P: Pattern + Clone, { fn format( &self, diff --git a/spdlog/src/formatter/pattern_formatter/runtime.rs b/spdlog/src/formatter/pattern_formatter/runtime.rs new file mode 100644 index 00000000..4ca49157 --- /dev/null +++ b/spdlog/src/formatter/pattern_formatter/runtime.rs @@ -0,0 +1,418 @@ +use std::convert::Infallible; + +use spdlog_internal::pattern_parser::{ + error::TemplateError, + parse::{Template, TemplateToken}, + BuiltInFormatter, BuiltInFormatterInner, Error as PatternParserError, + PatternKind as GenericPatternKind, PatternRegistry as GenericPatternRegistry, + Result as PatternParserResult, +}; + +use super::{Pattern, PatternContext, __pattern as pattern}; +use crate::{ + error::{BuildPatternErrorInner, Error}, + Record, Result, StringBuf, +}; +type Patterns = Vec>; +type PatternCreator = Box Box>; +type PatternRegistry = GenericPatternRegistry; +type PatternKind = GenericPatternKind; + +#[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists +/// A pattern built at runtime. +/// +/// If your pattern is known at compile-time and you don't need to build new +/// patterns from runtime input, consider using [`pattern!`] macro. +/// +/// # Usage +/// +/// The template string format and built-in patterns are consistent with +/// [`pattern!`] macro. The only difference is the way they are built and that +/// one of them is built at compile-time and the other at runtime. +/// +/// For other usage, please see the documentation of [`pattern!`] macro. +/// +/// ## Basic Usage +/// +/// ``` +/// # use spdlog::formatter::{PatternFormatter, RuntimePattern}; +/// use spdlog::info; +/// +/// # +#[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] +/// # fn main() -> Result<(), Box> { +/// let formatter = PatternFormatter::new(RuntimePattern::new("[{level}] {payload}{eol}")?); +/// # let (doctest, sink) = test_utils::echo_logger_from_formatter( +/// # Box::new(formatter), +/// # None +/// # ); +/// +/// info!(logger: doctest, "Interesting log message"); +/// # assert_eq!( +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message\n" +/// # ); +/// # Ok(()) } +/// ``` +/// +/// ## With Custom Patterns +/// +/// ``` +/// use std::fmt::Write; +/// +/// use spdlog::{ +/// formatter::{pattern, Pattern, PatternContext, PatternFormatter, RuntimePattern}, +/// Record, StringBuf, info +/// }; +/// +/// #[derive(Default, Clone)] +/// struct MyPattern; +/// +/// impl Pattern for MyPattern { +/// fn format(&self, record: &Record, dest: &mut StringBuf, _: &mut PatternContext) -> spdlog::Result<()> { +/// write!(dest, "My own pattern").map_err(spdlog::Error::FormatRecord) +/// } +/// } +/// +#[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))] +/// # fn main() -> Result<(), Box> { +/// let formatter = PatternFormatter::new( +/// RuntimePattern::builder() +/// .template("[{level}] {payload} - {$mypat1} {$mypat2}{eol}") +/// .custom_pattern("mypat1", MyPattern::default) +/// .custom_pattern("mypat2", || pattern!("[{level_short}-{level}]")) +/// .build()? +/// ); +/// # let (doctest, sink) = test_utils::echo_logger_from_formatter( +/// # Box::new(formatter), +/// # None +/// # ); +/// +/// info!(logger: doctest, "Interesting log message"); +/// # assert_eq!( +/// # sink.clone_string().replace("\r", ""), +/// /* Output */ "[info] Interesting log message - My own pattern [I-info]\n" +/// # ); +/// # Ok(()) } +/// ``` +/// +/// [`pattern!`]: crate::formatter::pattern +#[derive(Clone)] +pub struct RuntimePattern(Patterns); + +impl RuntimePattern { + /// Constructs a runtime pattern from a template string. + /// + /// About the template string format, please see the documentation of + /// [`pattern!`] macro. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn new(template: T) -> Result + where + T: Into, + { + Self::builder().template(template).build() + } + + /// Constructs a [`RuntimePatternBuilder`] to build a runtime pattern with + /// advanced parameters. + pub fn builder() -> RuntimePatternBuilder<()> { + RuntimePatternBuilder { + template: (), + custom_patterns: Vec::new(), + } + } +} + +impl Pattern for RuntimePattern { + fn format( + &self, + record: &Record, + dest: &mut StringBuf, + ctx: &mut PatternContext, + ) -> Result<()> { + for pattern in &self.0 { + pattern.format(record, dest, ctx)?; + } + Ok(()) + } +} + +#[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists +/// The builder of [`RuntimePattern`]. +#[doc = include_str!("../../include/doc/generic-builder-note.md")] +/// +/// # Example +/// +/// See the documentation of [`RuntimePattern`]. +pub struct RuntimePatternBuilder { + template: ArgT, + custom_patterns: Vec<(String, PatternCreator)>, +} + +impl RuntimePatternBuilder { + /// Specifies the template string. + /// + /// This parameter is **required**. + /// + /// About the template string format, please see the documentation of + /// [`pattern!`] macro. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn template(self, template: S) -> RuntimePatternBuilder + where + S: Into, + { + RuntimePatternBuilder { + template: template.into(), + custom_patterns: self.custom_patterns, + } + } + + /// Specifies a creator for a custom pattern that appears in the template + /// string. + /// + /// This parameter is **optional** if there is no reference to a custom + /// pattern in the template string, otherwise it's **required**. + /// + /// It is conceptually equivalent to `{$my_pat} => MyPattern::new` in + /// [`pattern!`] macro. + /// + /// The placeholder argument must be an identifier, e.g. `"my_pat"`, + /// `"_my_pat"`, etc., it cannot be `"2my_pat"`, `"r#my_pat"`, `"3"`, etc. + /// + /// [`pattern!`]: crate::formatter::pattern + pub fn custom_pattern(mut self, placeholder: S, pattern_creator: F) -> Self + where + S: Into, + P: Pattern + 'static, + F: Fn() -> P + 'static, + { + self.custom_patterns.push(( + placeholder.into(), + Box::new(move || Box::new(pattern_creator())), + )); + self + } +} + +impl RuntimePatternBuilder<()> { + #[doc(hidden)] + #[deprecated(note = "\n\n\ + builder compile-time error:\n\ + - missing required field `template`\n\n\ + ")] + pub fn build(self, _: Infallible) {} +} + +impl RuntimePatternBuilder { + /// Builds a runtime pattern. + pub fn build(self) -> Result { + self.build_inner() + } + + fn build_inner(self) -> Result { + let mut registry = PatternRegistry::with_builtin(); + for (name, formatter) in self.custom_patterns { + if !(!name.is_empty() + && name + .chars() + .next() + .map(|ch| ch.is_ascii_alphabetic() || ch == '_') + .unwrap() + && name + .chars() + .skip(1) + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')) + { + return Err(Error::err_build_pattern( + BuildPatternErrorInner::InvalidCustomPlaceholder(name), + )); + } + registry + .register_custom(name, formatter) + .map_err(Error::err_build_pattern_internal)?; + } + + let template = + Template::parse(&self.template).map_err(Error::err_build_pattern_internal)?; + + Synthesiser::new(registry) + .synthesize(template) + .map_err(Error::err_build_pattern_internal) + .map(RuntimePattern) + } +} + +struct Synthesiser { + registry: PatternRegistry, +} + +impl Synthesiser { + fn new(registry: PatternRegistry) -> Self { + Self { registry } + } + + fn synthesize(&self, template: Template) -> PatternParserResult { + self.build_patterns(template, false) + } + + fn build_patterns( + &self, + template: Template, + mut style_range_seen: bool, + ) -> PatternParserResult { + let mut patterns = Patterns::new(); + + for token in template.tokens { + let pattern = match token { + TemplateToken::Literal(t) => Box::new(t.literal), + TemplateToken::Formatter(t) => { + let pattern = self.registry.find(t.has_custom_prefix, t.placeholder)?; + match pattern { + PatternKind::BuiltIn(builtin) => build_builtin_pattern(builtin), + PatternKind::Custom { factory, .. } => factory(), + } + } + TemplateToken::StyleRange(style_range) => { + if style_range_seen { + return Err(PatternParserError::Template( + TemplateError::MultipleStyleRange, + )); + } + style_range_seen = true; + Box::new(pattern::StyleRange::new( + self.build_patterns(style_range.body, true)?, + )) + } + }; + patterns.push(pattern); + } + + Ok(patterns) + } +} + +fn build_builtin_pattern(builtin: &BuiltInFormatter) -> Box { + macro_rules! match_builtin { + ( $($name:ident),+ $(,)? ) => { + match builtin.inner() { + $(BuiltInFormatterInner::$name => Box::::default()),+ + } + }; + } + + match_builtin!( + AbbrWeekdayName, + WeekdayName, + AbbrMonthName, + MonthName, + FullDateTime, + ShortYear, + Year, + ShortDate, + Date, + Month, + Day, + Hour, + Hour12, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, + AmPm, + Time12, + ShortTime, + Time, + TzOffset, + UnixTimestamp, + Full, + Level, + ShortLevel, + Source, + SourceFilename, + SourceFile, + SourceLine, + SourceColumn, + SourceModulePath, + LoggerName, + Payload, + ProcessId, + ThreadId, + Eol + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn builder(template: &str) -> RuntimePatternBuilder { + RuntimePattern::builder().template(template) + } + + fn new(template: &str) -> Result { + RuntimePattern::new(template) + } + + fn custom_pat_creator() -> impl Pattern { + pattern::Level + } + + #[test] + fn valid() { + assert!(new("").is_ok()); + assert!(new("{logger}").is_ok()); + assert!(builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .build() + .is_ok()); + assert!(builder("{logger} {$_custom_pat}") + .custom_pattern("_custom_pat", custom_pat_creator) + .build() + .is_ok()); + assert!(builder("{logger} {$_2custom_pat}") + .custom_pattern("_2custom_pat", custom_pat_creator) + .build() + .is_ok()); + } + + #[test] + fn invalid() { + assert!(matches!(new("{logger-name}"), Err(Error::BuildPattern(_)))); + assert!(matches!(new("{nonexistent}"), Err(Error::BuildPattern(_)))); + assert!(matches!(new("{}"), Err(Error::BuildPattern(_)))); + assert!(matches!( + new("{logger} {$custom_pat_no_ref}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("custom-pat2", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$custom_pat}") + .custom_pattern("custom_pat", custom_pat_creator) + .custom_pattern("2custom_pat", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + builder("{logger} {$r#custom_pat}") + .custom_pattern("r#custom_pat", custom_pat_creator) + .build(), + Err(Error::BuildPattern(_)) + )); + } +} diff --git a/spdlog/src/lib.rs b/spdlog/src/lib.rs index d27745ef..9b115ff5 100644 --- a/spdlog/src/lib.rs +++ b/spdlog/src/lib.rs @@ -58,6 +58,16 @@ //! [open a discussion]. For feature requests or bug reports, please [open an //! issue]. //! +//! # Overview of features +//! +//! - [Compatible with log crate](#compatible-with-log-crate) +//! - [Asynchronous support](#asynchronous-support) +//! - [Configured via environment +//! variable](#configured-via-environment-variable) +//! - [Compile-time and runtime pattern +//! formatter](#compile-time-and-runtime-pattern-formatter) +//! - [Compile-time filters](#compile-time-filters) +//! //! # Compatible with log crate //! //! This is optional and is controlled by crate feature `log`. @@ -85,7 +95,35 @@ //! //! For more details, see the documentation of [`init_env_level`]. //! -//! # Compile time filters +//! # Compile-time and runtime pattern formatter +//! +//! spdlog-rs supports formatting your log records according to a pattern +//! string. There are 2 ways to construct a pattern: +//! +//! - Macro [`pattern!`]: Builds a pattern at compile-time. +//! - Struct [`RuntimePattern`]: A pattern built at runtime. +//! +//! ``` +//! use spdlog::formatter::{pattern, PatternFormatter}; +//! #[cfg(feature = "runtime-pattern")] +//! use spdlog::formatter::RuntimePattern; +//! # use spdlog::sink::{Sink, WriteSink}; +//! +//! # fn main() -> Result<(), Box> { +//! // This pattern is built at compile-time, the template accepts only a literal string. +//! let pattern = pattern!("[{date} {time}.{millisecond}] [{level}] {payload}{eol}"); +//! +//! // This pattern is built at runtime, the template accepts a runtime string. +//! #[cfg(feature = "runtime-pattern")] +//! let pattern = RuntimePattern::new("[{date} {time}.{millisecond}] [{level}] {payload}{eol}")?; +//! +//! // Use the compile-time or runtime pattern. +//! # let your_sink = WriteSink::builder().target(vec![]).build()?; +//! your_sink.set_formatter(Box::new(PatternFormatter::new(pattern))); +//! # Ok(()) } +//! ``` +//! +//! # Compile-time filters //! //! Log levels can be statically disabled at compile time via Cargo features. //! Log invocations at disabled levels will be skipped and will not even be @@ -139,6 +177,9 @@ //! features need to be enabled as well. See the documentation of the //! component for these details. //! +//! - `runtime-pattern` enables the ability to build patterns with runtime +//! template string. See [`RuntimePattern`] for more details. +//! //! # Supported Rust Versions //! //! tests/compile_fail/pattern_macro_syntax.rs:8:79 + | +8 | pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$} => custom_pat_creator); + | ^ + +error: unexpected token + --> tests/compile_fail/pattern_macro_syntax.rs:9:85 + | +9 | pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$custom-pat2} => custom_pat_creator); + | ^ + +error: expected identifier + --> tests/compile_fail/pattern_macro_syntax.rs:10:79 + | +10 | pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$2custom_pat} => custom_pat_creator); + | ^^^^^^^^^^^ diff --git a/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.rs b/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.rs new file mode 100644 index 00000000..3d24dd32 --- /dev/null +++ b/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.rs @@ -0,0 +1,13 @@ +use spdlog::formatter::{runtime_pattern, Pattern}; + +fn custom_pat_creator() -> impl Pattern { + unimplemented!() +} + +fn runtime_pattern() { + runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$} => custom_pat_creator); + runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$custom-pat2} => custom_pat_creator); + runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$2custom_pat} => custom_pat_creator); +} + +fn main() {} diff --git a/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.stderr b/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.stderr new file mode 100644 index 00000000..961a1663 --- /dev/null +++ b/spdlog/tests/compile_fail/pattern_runtime_macro_syntax.stderr @@ -0,0 +1,17 @@ +error: unexpected end of input, expected identifier + --> tests/compile_fail/pattern_runtime_macro_syntax.rs:8:87 + | +8 | runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$} => custom_pat_creator); + | ^ + +error: unexpected token + --> tests/compile_fail/pattern_runtime_macro_syntax.rs:9:93 + | +9 | runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$custom-pat2} => custom_pat_creator); + | ^ + +error: expected identifier + --> tests/compile_fail/pattern_runtime_macro_syntax.rs:10:87 + | +10 | runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator, {$2custom_pat} => custom_pat_creator); + | ^^^^^^^^^^^ diff --git a/spdlog/tests/pattern.rs b/spdlog/tests/pattern.rs index 0b40f04c..655a3d05 100644 --- a/spdlog/tests/pattern.rs +++ b/spdlog/tests/pattern.rs @@ -7,13 +7,13 @@ use std::{ use cfg_if::cfg_if; use regex::Regex; #[cfg(feature = "runtime-pattern")] -use spdlog::formatter::RuntimePattern; +use spdlog::formatter::runtime_pattern; use spdlog::{ error, formatter::{pattern, Formatter, Pattern, PatternFormatter}, prelude::*, sink::Sink, - StringBuf, __EOL, + Error, StringBuf, __EOL, }; include!(concat!( @@ -25,7 +25,7 @@ macro_rules! test_pattern { ( $template:literal, $($args: expr),+ $(,)? ) => { test_pattern_inner(pattern!($template), $($args),+); #[cfg(feature = "runtime-pattern")] - test_pattern_inner(RuntimePattern::new($template).unwrap(), $($args),+); + test_pattern_inner(runtime_pattern!($template).unwrap(), $($args),+); }; ( $patterns:expr, $($args: expr),+ $(,)? ) => { $patterns.into_iter().for_each(|pat| { @@ -50,29 +50,38 @@ fn test_builtin_formatters() { #[test] fn test_custom_formatters() { + let mut patterns = vec![Box::new( + pattern!("{logger}: [{level}] hello {payload} - {$mock1} / {$mock2}", + {$mock1} => MockPattern1::default, + {$mock2} => MockPattern2::default, + ), + ) as Box]; + + #[cfg(feature = "runtime-pattern")] + patterns.push(Box::new( + runtime_pattern!("{logger}: [{level}] hello {payload} - {$mock1} / {$mock2}", + {$mock1} => MockPattern1::default, + {$mock2} => MockPattern2::default, + ) + .unwrap(), + )); + test_pattern!( - [ - Box::new( - pattern!("{logger}: [{level}] hello {payload} - {$mock1} / {$mock2}", - {$mock1} => MockPattern1::default, - {$mock2} => MockPattern2::default, - ) - ) as Box, - #[cfg(feature = "runtime-pattern")] - Box::new( - RuntimePattern::builder() - .template("{logger}: [{level}] hello {payload} - {$mock1} / {$mock2}") - .custom_pattern("mock1", MockPattern1::default) - .custom_pattern("mock2", MockPattern2::default) - .build() - .unwrap() - ) - ], + patterns, "logger_name: [error] hello record_payload - mock_pattern_1 / mock_pattern_2", None, ); } +#[cfg(feature = "runtime-pattern")] +#[test] +fn test_unknown_custom_formatter() { + let pattern = runtime_pattern!("{logger}: [{level}] hello {payload} - {$mock1} / {$mock2}", + {$mock1} => MockPattern1::default, + ); + assert!(pattern.is_err()); +} + #[test] fn test_style_range() { test_pattern!( @@ -302,7 +311,7 @@ fn test_builtin_patterns() { ( $template:literal, $($args: expr),+ $(,)? ) => { check_inner(pattern!($template), $($args),+); #[cfg(feature = "runtime-pattern")] - check_inner(RuntimePattern::new($template).unwrap(), $($args),+); + check_inner(runtime_pattern!($template).unwrap(), $($args),+); }; } @@ -443,6 +452,56 @@ fn test_builtin_patterns() { check!("{eol}", Some("{eol}"), vec![]); } +#[cfg(feature = "runtime-pattern")] +fn custom_pat_creator() -> impl Pattern { + spdlog::formatter::__pattern::Level +} + +#[cfg(feature = "runtime-pattern")] +#[test] +fn runtime_pattern_valid() { + assert!(runtime_pattern!("").is_ok()); + assert!(runtime_pattern!("{logger}").is_ok()); + assert!( + runtime_pattern!("{logger} {$custom_pat}", {$custom_pat} => custom_pat_creator).is_ok() + ); + assert!( + runtime_pattern!("{logger} {$_custom_pat}", {$_custom_pat} => custom_pat_creator).is_ok() + ); + assert!( + runtime_pattern!("{logger} {$_2custom_pat}", {$_2custom_pat} => custom_pat_creator).is_ok() + ); +} + +#[cfg(feature = "runtime-pattern")] +#[test] +fn runtime_pattern_invalid() { + assert!(matches!( + runtime_pattern!("{logger-name}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + runtime_pattern!("{nonexistent}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + runtime_pattern!("{}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + runtime_pattern!("{logger} {$custom_pat_no_ref}"), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + runtime_pattern!("{logger} {$custom_pat}", {$r#custom_pat} => custom_pat_creator), + Err(Error::BuildPattern(_)) + )); + assert!(matches!( + runtime_pattern!("{logger} {$r#custom_pat}", {$r#custom_pat} => custom_pat_creator), + Err(Error::BuildPattern(_)) + )); +} + #[cfg(feature = "multi-thread")] #[test] fn test_different_context_thread() { From 1d1ad02c7b05f9c1dc36a2c734f4dc1d6091e549 Mon Sep 17 00:00:00 2001 From: Asuna Date: Thu, 21 Mar 2024 07:15:44 +0800 Subject: [PATCH 4/4] Use `Box::{leak,from_raw}` to create self-reference for `Pattern` parser --- spdlog-macros/Cargo.toml | 1 - spdlog-macros/src/pattern/mod.rs | 52 +++++++++++++++++--------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/spdlog-macros/Cargo.toml b/spdlog-macros/Cargo.toml index 5c84af29..2c811679 100644 --- a/spdlog-macros/Cargo.toml +++ b/spdlog-macros/Cargo.toml @@ -17,6 +17,5 @@ proc-macro = true nom = "7.1.1" proc-macro2 = "1.0.47" quote = "1.0.21" -self_cell = "1.0.1" spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal" } syn = { version = "2.0.38", features = ["full"] } diff --git a/spdlog-macros/src/pattern/mod.rs b/spdlog-macros/src/pattern/mod.rs index d82315b8..978be0d8 100644 --- a/spdlog-macros/src/pattern/mod.rs +++ b/spdlog-macros/src/pattern/mod.rs @@ -2,7 +2,6 @@ mod synthesis; use proc_macro2::{Span, TokenStream}; use quote::quote; -use self_cell::self_cell; use spdlog_internal::pattern_parser::{ check_custom_pattern_names, parse::Template, PatternRegistry, Result, }; @@ -71,26 +70,18 @@ pub fn runtime_pattern_impl(runtime_pattern: RuntimePattern) -> Result)>, /// Any user-provided pattern-to-formatter mapping. custom_patterns: CustomPatterns, } -self_cell! { - pub struct TemplateSelfRef { - owner: String, - #[covariant] - dependent: Template, - } -} - impl Pattern { fn custom_patterns(&self) -> impl IntoIterator { self.custom_patterns.0.iter() } fn template(&self) -> &Template { - self.template.borrow_dependent() + &self.template.as_ref().unwrap().1 } } @@ -100,20 +91,33 @@ impl Parse for Pattern { input.parse::>()?; let custom_patterns = input.parse()?; - let ret = Pattern { - template: TemplateSelfRef::try_new(template_lit.value(), |template_str| { - Template::parse(template_str).map_err(|err| { - syn::Error::new( - // TODO: Maybe we can make a subspan for the literal for a better error - // message - template_lit.span(), - err, - ) - }) - })?, + // Struct `Template` have almost no way of owning a `String`, we have to store + // `template_lit` somewhere. Here we use `Box::leak` + `Box::from_raw` to create + // a simple self-reference. + let template_lit_leaked = Box::leak(Box::new(template_lit.value())); + + let template = Template::parse(template_lit_leaked).map_err(|err| { + syn::Error::new( + // TODO: Maybe we can make a subspan for the literal for a better error message + template_lit.span(), + err, + ) + })?; + + Ok(Pattern { + template: Some((template_lit_leaked, template)), custom_patterns, - }; - Ok(ret) + }) + } +} + +impl Drop for Pattern { + fn drop(&mut self) { + let (template_lit_leaked, template) = self.template.take().unwrap(); + // Drop the user of the leaked data first. + drop(template); + // Restore the ownership of the leaked `String` and then drop it. + drop(unsafe { Box::from_raw(template_lit_leaked as *const String as *mut String) }); } }