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/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..a60da547 --- /dev/null +++ b/spdlog-internal/src/pattern_parser/error.rs @@ -0,0 +1,164 @@ +use std::fmt::{self, Display}; + +use nom::error::Error as NomError; +use thiserror::Error; + +use super::PatternKind; +use crate::impossible; + +#[derive(Error, Debug, PartialEq)] +pub enum Error { + ConflictName { + existing: PatternKind<()>, + incoming: PatternKind<()>, + }, + Template(TemplateError), + Parse(NomError), + Multiple(Vec), + #[cfg(test)] + __ForInternalTestsUseOnly(usize), +} + +impl Error { + pub fn push_err(result: Result, new: Self) -> Result { + match result { + Ok(_) => Err(new), + Err(Self::Multiple(mut errors)) => { + errors.push(new); + Err(Self::Multiple(errors)) + } + Err(prev) => Err(Error::Multiple(vec![prev, new])), + } + } + + pub fn push_result(result: Result, new: Result) -> Result { + match new { + Ok(_) => result, + Err(err) => Self::push_err(result, err), + } + } +} + +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) + } + Error::Multiple(errs) => { + writeln!(f, "{} errors detected:", errs.len())?; + for err in errs { + writeln!(f, " - {}", err)?; + } + Ok(()) + } + #[cfg(test)] + Error::__ForInternalTestsUseOnly(value) => { + write!(f, "{}", value) + } + } + } +} + +#[derive(Error, Debug, Eq, PartialEq)] +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; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn push_err() { + macro_rules! make_err { + ( $($inputs:tt)+ ) => { + Error::__ForInternalTestsUseOnly($($inputs)*) + }; + } + + assert!(matches!( + Error::push_err(Ok(()), make_err!(1)), + Err(make_err!(1)) + )); + + assert!(matches!( + Error::push_err::<()>(Err(make_err!(1)), make_err!(2)), + Err(Error::Multiple(v)) if matches!(v[..], [make_err!(1), make_err!(2)]) + )); + } +} 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..bfd8fbd3 --- /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::{check_custom_pattern_names, 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..734a5ee3 --- /dev/null +++ b/spdlog-internal/src/pattern_parser/registry.rs @@ -0,0 +1,265 @@ +use std::{ + borrow::Cow, + collections::{hash_map::Entry, HashMap}, + fmt::Debug, + hash::Hash, +}; + +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)); + } + } + } +} + +pub fn check_custom_pattern_names(names: I) -> Result<()> +where + N: AsRef + Eq + PartialEq + Hash, + I: IntoIterator, +{ + let mut seen_names: HashMap = HashMap::new(); + let mut result = Ok(()); + + for name in names { + if let Some(existing) = BuiltInFormatter::iter().find(|f| f.placeholder() == name.as_ref()) + { + result = Error::push_err( + result, + Error::ConflictName { + existing: PatternKind::BuiltIn(existing), + incoming: PatternKind::Custom { + placeholder: Cow::Owned(name.as_ref().into()), + factory: (), + }, + }, + ); + } + + if let Some(seen_count) = seen_names.get_mut(&name) { + *seen_count += 1; + if *seen_count == 2 { + let conflict_pattern = PatternKind::Custom { + placeholder: Cow::Owned(name.as_ref().into()), + factory: (), + }; + result = Error::push_err( + result, + Error::ConflictName { + existing: conflict_pattern.clone(), + incoming: conflict_pattern, + }, + ); + } + } else { + seen_names.insert(name, 1); + } + } + + debug_assert!(seen_names.iter().all(|(_, seen_count)| *seen_count == 1) || result.is_err()); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pattern_parser::BuiltInFormatterInner; + + #[test] + fn custom_pattern_names_checker() { + use check_custom_pattern_names as check; + + assert!(check(["a", "b"]).is_ok()); + assert_eq!( + check(["a", "a"]), + Err(Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "a".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "a".into(), + factory: () + } + }) + ); + assert_eq!( + check(["a", "b", "a"]), + Err(Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "a".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "a".into(), + factory: () + } + }) + ); + assert_eq!( + check(["date"]), + Err(Error::ConflictName { + existing: PatternKind::BuiltIn(BuiltInFormatter(BuiltInFormatterInner::Date)), + incoming: PatternKind::Custom { + placeholder: "date".into(), + factory: () + } + }) + ); + assert_eq!( + check(["date", "a", "a"]), + Err(Error::Multiple(vec![ + Error::ConflictName { + existing: PatternKind::BuiltIn(BuiltInFormatter(BuiltInFormatterInner::Date)), + incoming: PatternKind::Custom { + placeholder: "date".into(), + factory: () + } + }, + Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "a".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "a".into(), + factory: () + } + } + ])) + ); + assert_eq!( + check(["date", "a", "a", "a"]), + Err(Error::Multiple(vec![ + Error::ConflictName { + existing: PatternKind::BuiltIn(BuiltInFormatter(BuiltInFormatterInner::Date)), + incoming: PatternKind::Custom { + placeholder: "date".into(), + factory: () + } + }, + Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "a".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "a".into(), + factory: () + } + } + ])) + ); + assert_eq!( + check(["b", "date", "a", "b", "a", "a"]), + Err(Error::Multiple(vec![ + Error::ConflictName { + existing: PatternKind::BuiltIn(BuiltInFormatter(BuiltInFormatterInner::Date)), + incoming: PatternKind::Custom { + placeholder: "date".into(), + factory: () + } + }, + Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "b".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "b".into(), + factory: () + } + }, + Error::ConflictName { + existing: PatternKind::Custom { + placeholder: "a".into(), + factory: () + }, + incoming: PatternKind::Custom { + placeholder: "a".into(), + factory: () + } + } + ])) + ); + } +} diff --git a/spdlog-macros/Cargo.toml b/spdlog-macros/Cargo.toml index ed9d223d..2c811679 100644 --- a/spdlog-macros/Cargo.toml +++ b/spdlog-macros/Cargo.toml @@ -17,4 +17,5 @@ proc-macro = true nom = "7.1.1" proc-macro2 = "1.0.47" quote = "1.0.21" +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..cc1eec0d 100644 --- a/spdlog-macros/src/lib.rs +++ b/spdlog-macros/src/lib.rs @@ -5,35 +5,29 @@ //! //! [`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}, -}; +use proc_macro2::TokenStream as TokenStream2; +use spdlog_internal::pattern_parser::Result; #[proc_macro] pub fn pattern(input: TokenStream) -> TokenStream { - let pat = syn::parse_macro_input!(input as Pattern); + let pattern = syn::parse_macro_input!(input); + into_or_error(pattern::pattern_impl(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); - } - } +#[proc_macro] +pub fn runtime_pattern(input: TokenStream) -> TokenStream { + // We must make this macro a procedural macro because macro cannot match the "$" + // 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)) +} - match synthesiser.synthesis(&pat.template) { +fn into_or_error(result: Result) -> TokenStream { + match result { 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..978be0d8 --- /dev/null +++ b/spdlog-macros/src/pattern/mod.rs @@ -0,0 +1,181 @@ +mod synthesis; + +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use spdlog_internal::pattern_parser::{ + check_custom_pattern_names, parse::Template, PatternRegistry, Result, +}; +use syn::{ + braced, + parse::{Parse, ParseStream}, + Expr, 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()) +} + +pub fn runtime_pattern_impl(runtime_pattern: RuntimePattern) -> Result { + let custom_pattern_names = runtime_pattern + .custom_patterns + .0 + .iter() + .map(|(name, _)| name.to_string()); + check_custom_pattern_names(custom_pattern_names)?; + + let custom_pattern_register_calls: Vec<_> = runtime_pattern + .custom_patterns + .0 + .into_iter() + .map(|(name, factory)| { + let name_literal = LitStr::new(&name.to_string(), Span::mixed_site()); + quote! { + registry.register_custom(#name_literal, Box::new(|| Box::new(#factory()))) + .expect("unexpected panic, please report a bug to spdlog-rs"); + } + }) + .collect(); + + let template = runtime_pattern.template; + Ok(quote! { + { + let template = #template; + let pattern_registry = { + let mut registry = spdlog_internal + ::pattern_parser + ::PatternRegistry + :: Box>> + ::with_builtin(); + #(#custom_pattern_register_calls)* + registry + }; + spdlog::formatter::RuntimePattern::__with_custom_patterns( + template, + pattern_registry, + ) + } + }) +} + +/// 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: Option<(&'static String, Template<'static>)>, + /// Any user-provided pattern-to-formatter mapping. + custom_patterns: CustomPatterns, +} + +impl Pattern { + fn custom_patterns(&self) -> impl IntoIterator { + self.custom_patterns.0.iter() + } + + fn template(&self) -> &Template { + &self.template.as_ref().unwrap().1 + } +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> syn::Result { + let template_lit = input.parse::()?; + input.parse::>()?; + let custom_patterns = input.parse()?; + + // 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, + }) + } +} + +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) }); + } +} + +/// A parsed runtime pattern. +/// +/// The only difference between a pattern and a runtime pattern is that the +/// template string of a pattern must be a string literal, while the template +/// string of a runtime pattern can be a runtime expression that evaluates to a +/// string. +pub struct RuntimePattern { + template: Expr, + custom_patterns: CustomPatterns, +} + +impl Parse for RuntimePattern { + fn parse(input: ParseStream) -> syn::Result { + let template_expr = input.parse::()?; + input.parse::>()?; + let custom_patterns = input.parse()?; + + let ret = RuntimePattern { + template: template_expr, + 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 {} diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 79c01742..cbce1f8c 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,8 @@ flexi_logger = "=0.24.1" tracing = "=0.1.37" tracing-subscriber = "=0.3.16" tracing-appender = "=0.2.2" +paste = "1.0.14" +trybuild = "1.0.90" [build-dependencies] rustc_version = "0.4.0" diff --git a/spdlog/benches/pattern.rs b/spdlog/benches/pattern.rs index f5b1ea05..086ba8a3 100644 --- a/spdlog/benches/pattern.rs +++ b/spdlog/benches/pattern.rs @@ -4,12 +4,14 @@ 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, }; +use spdlog_macros::runtime_pattern; use test::Bencher; include!(concat!( @@ -76,15 +78,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 +97,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, + runtime_pattern!("[{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, runtime_pattern!($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..65fd60fd 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 [`runtime_pattern!`] when the + /// pattern is failed to be built at runtime. + /// + /// [`runtime_pattern!`]: crate::formatter::runtime_pattern + #[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), @@ -242,6 +250,13 @@ 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(pub(crate) spdlog_internal::pattern_parser::Error); + /// 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..922b8a96 100644 --- a/spdlog/src/formatter/pattern_formatter/mod.rs +++ b/spdlog/src/formatter/pattern_formatter/mod.rs @@ -14,15 +14,22 @@ #[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, }; #[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists -/// Build a pattern from a template string at compile-time. +/// Build a pattern from a template literal string at compile-time. /// /// It accepts inputs in the form: /// @@ -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 +/// [`runtime_pattern!`] macro 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. /// +/// [`runtime_pattern!`]: crate::formatter::runtime_pattern /// [`FullFormatter`]: crate::formatter::FullFormatter pub use ::spdlog_macros::pattern; @@ -364,8 +366,11 @@ 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 with a literal template string + /// at compile-time. + /// - Macro [`runtime_pattern!`] to build a pattern at runtime. #[must_use] pub fn new(pattern: P) -> Self { Self { pattern } @@ -425,10 +430,13 @@ impl PatternContext { /// /// # Custom Patterns /// -/// There are 2 approaches to create your own pattern: +/// There are 3 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 { +/// - Use the [`pattern`] macro to create a pattern from a literal template +/// string. +/// - Use the [`runtime_pattern`] macro to create a pattern from a runtime +/// template string. +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 +449,7 @@ pub trait Pattern: Send + Sync { ctx: &mut PatternContext, ) -> crate::Result<()>; } +clone_trait_object!(Pattern); impl Pattern for String { fn format( @@ -478,23 +487,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 +505,7 @@ where impl Pattern for Arc where T: ?Sized + Pattern, + Self: Clone, { fn format( &self, @@ -520,9 +517,10 @@ where } } -impl Pattern for [T] +impl Pattern for &[T] where T: Pattern, + Self: Clone, { fn format( &self, @@ -530,7 +528,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 +538,7 @@ where impl Pattern for [T; N] where T: Pattern, + Self: Clone, { fn format( &self, @@ -547,13 +546,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 +564,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 +591,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 +1263,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..ef768e7e --- /dev/null +++ b/spdlog/src/formatter/pattern_formatter/runtime.rs @@ -0,0 +1,260 @@ +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::{BuildPatternError, Error}, + Record, Result, StringBuf, +}; + +type Patterns = Vec>; +type PatternCreator = Box Box>; +type PatternRegistry = GenericPatternRegistry; +type PatternKind = GenericPatternKind; + +/// Build a pattern from a template string at runtime. +/// +/// It accepts inputs in the form: +/// +/// ```ignore +/// // This is not exactly a valid declarative macro, just for intuition. +/// macro_rules! runtime_pattern { +/// ( $template:expr $(,)? ) => {}; +/// ( $template:expr, $( {$$custom:ident} => $ctor:expr ),+ $(,)? ) => {}; +/// } +/// ``` +/// +/// The only difference between `runtime_pattern!` macro and [`pattern!`] macro +/// is that [`pattern!`] macro only accepts a string literal as the pattern +/// template, while `runtime_pattern!` macro accepts an expression that can be +/// evaluated to the pattern template string at runtime. +/// +/// The returen type of `runtime_pattern!` macro is +/// `Result`. An error will be returned when +/// parsing of the template string fails. If any of the custom patterns given +/// are invalid, a compilation error will be triggered. +/// +/// For the input formats and more usages, please refer to [`pattern!`] macro. +/// +/// # Example +/// +/// ``` +/// use spdlog::formatter::{runtime_pattern, PatternFormatter}; +/// +/// # type MyPattern = spdlog::formatter::__pattern::Level; +/// # fn main() -> Result<(), Box> { +/// let template = String::from("[{level}] {payload} - {$mypat}{eol}"); +/// let pat = runtime_pattern!(&template, {$mypat} => MyPattern::default)?; +/// let formatter = PatternFormatter::new(pat); +/// # Ok(()) } +/// ``` +/// +/// [`pattern!`]: crate::formatter::pattern +pub use spdlog_macros::runtime_pattern; + +#[rustfmt::skip] // rustfmt currently breaks some empty lines if `#[doc = include_str!("xxx")]` exists +/// A runtime pattern built via [`runtime_pattern!`] macro. +/// +/// ## Basic Usage +/// +/// ``` +/// # use spdlog::formatter::{runtime_pattern, PatternFormatter}; +/// 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(runtime_pattern!("[{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, runtime_pattern, 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 template = "[{level}] {payload} - {$mypat1} {$mypat2}{eol}"; +/// # // TODO: Directly pass the closure to runtime_pattern! macro +/// fn pat() -> impl Pattern { pattern!("[{level_short}-{level}]") } +/// let formatter = PatternFormatter::new( +/// runtime_pattern!( +/// template, +/// {$mypat1} => MyPattern::default, +/// {$mypat2} => pat +/// )? +/// ); +/// # 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 { + // Private function, do not use in your code directly. + #[doc(hidden)] + pub fn __with_custom_patterns(template: &str, registry: PatternRegistry) -> Result { + Template::parse(template) + .and_then(|template| { + Synthesiser::new(registry) + .synthesize(template) + .map(RuntimePattern) + }) + .map_err(|err| Error::BuildPattern(BuildPatternError(err))) + } +} + +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(()) + } +} + +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 + ) +} diff --git a/spdlog/src/lib.rs b/spdlog/src/lib.rs index d27745ef..b290ebb4 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,38 @@ //! //! 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. +//! - Macro [`runtime_pattern!`]: Builds a pattern at runtime. +//! +//! ``` +//! use spdlog::formatter::{pattern, PatternFormatter}; +//! #[cfg(feature = "runtime-pattern")] +//! use spdlog::formatter::runtime_pattern; +//! # 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}"); +//! +//! #[cfg(feature = "runtime-pattern")] +//! { +//! // This pattern is built at runtime, the template accepts a runtime string. +//! let input = "[{date} {time}.{millisecond}] [{level}] {payload}{eol}"; +//! let pattern = runtime_pattern!(input)?; +//! } +//! +//! // 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 +180,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 aff89f2d..655a3d05 100644 --- a/spdlog/tests/pattern.rs +++ b/spdlog/tests/pattern.rs @@ -6,12 +6,14 @@ use std::{ use cfg_if::cfg_if; use regex::Regex; +#[cfg(feature = "runtime-pattern")] +use spdlog::formatter::runtime_pattern; use spdlog::{ error, formatter::{pattern, Formatter, Pattern, PatternFormatter}, prelude::*, sink::Sink, - StringBuf, __EOL, + Error, StringBuf, __EOL, }; include!(concat!( @@ -19,15 +21,28 @@ include!(concat!( "/test_utils/common_for_integration_test.rs" )); +macro_rules! test_pattern { + ( $template:literal, $($args: expr),+ $(,)? ) => { + test_pattern_inner(pattern!($template), $($args),+); + #[cfg(feature = "runtime-pattern")] + test_pattern_inner(runtime_pattern!($template).unwrap(), $($args),+); + }; + ( $patterns:expr, $($args: expr),+ $(,)? ) => { + $patterns.into_iter().for_each(|pat| { + test_pattern_inner(pat, $($args),+); + }); + }; +} + #[test] fn test_basic() { - test_pattern(pattern!("hello"), "hello", None); + test_pattern!("hello", "hello", None); } #[test] fn test_builtin_formatters() { - test_pattern( - pattern!("{logger}: [{level}] hello {payload}"), + test_pattern!( + "{logger}: [{level}] hello {payload}", "logger_name: [error] hello record_payload", None, ); @@ -35,27 +50,49 @@ fn test_builtin_formatters() { #[test] fn test_custom_formatters() { - test_pattern( + 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!( + 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( - pattern!("{logger}: [{level}] {^hello} {payload}"), + test_pattern!( + "{logger}: [{level}] {^hello} {payload}", "logger_name: [error] hello record_payload", Some(21..26), ); } #[track_caller] -fn test_pattern(pat: P, expect_formatted: F, expect_style_range: Option>) +fn test_pattern_inner(pat: P, expect_formatted: F, expect_style_range: Option>) where P: Pattern + 'static + Clone, F: AsRef, @@ -207,7 +244,7 @@ fn test_builtin_patterns() { const AM_PM: [&str; 2] = ["AM", "PM"]; #[track_caller] - fn check( + fn check_inner( pattern: impl Pattern + Clone + 'static, template: Option>, ranges: Vec>, @@ -270,6 +307,14 @@ fn test_builtin_patterns() { } } + macro_rules! check { + ( $template:literal, $($args: expr),+ $(,)? ) => { + check_inner(pattern!($template), $($args),+); + #[cfg(feature = "runtime-pattern")] + check_inner(runtime_pattern!($template).unwrap(), $($args),+); + }; + } + const YEAR_RANGE: RangeInclusive = 2022..=9999; const YEAR_SHORT_RANGE: RangeInclusive = 0..=99; const MONTH_RANGE: RangeInclusive = 1..=12; @@ -285,20 +330,12 @@ fn test_builtin_patterns() { const SOURCE_RANGE: RangeInclusive = 0..=9999; const OS_ID_RANGE: RangeInclusive = 1..=u64::MAX; - check(pattern!("{weekday_name}"), Some("{weekday_name}"), vec![]); - check( - pattern!("{weekday_name_full}"), - Some("{weekday_name_full}"), - vec![], - ); - check(pattern!("{month_name}"), Some("{month_name}"), vec![]); - check( - pattern!("{month_name_full}"), - Some("{month_name_full}"), - vec![], - ); - check( - pattern!("{datetime}"), + check!("{weekday_name}", Some("{weekday_name}"), vec![]); + check!("{weekday_name_full}", Some("{weekday_name_full}"), vec![]); + check!("{month_name}", Some("{month_name}"), vec![]); + check!("{month_name_full}", Some("{month_name_full}"), vec![]); + check!( + "{datetime}", Some("{weekday_name} {month_name} 00 00:00:00 0000"), vec![ DAY_RANGE, @@ -308,70 +345,58 @@ fn test_builtin_patterns() { YEAR_RANGE, ], ); - check(pattern!("{year_short}"), Some("00"), vec![YEAR_SHORT_RANGE]); - check(pattern!("{year}"), Some("0000"), vec![YEAR_RANGE]); - check( - pattern!("{date_short}"), + check!("{year_short}", Some("00"), vec![YEAR_SHORT_RANGE]); + check!("{year}", Some("0000"), vec![YEAR_RANGE]); + check!( + "{date_short}", Some("00/00/00"), vec![MONTH_RANGE, DAY_RANGE, YEAR_SHORT_RANGE], ); - check( - pattern!("{date}"), + check!( + "{date}", Some("0000-00-00"), vec![YEAR_RANGE, MONTH_RANGE, DAY_RANGE], ); - check(pattern!("{month}"), Some("00"), vec![MONTH_RANGE]); - check(pattern!("{day}"), Some("00"), vec![DAY_RANGE]); - check(pattern!("{hour}"), Some("00"), vec![HOUR_RANGE]); - check(pattern!("{hour_12}"), Some("00"), vec![HOUR_12_RANGE]); - check(pattern!("{minute}"), Some("00"), vec![MINUTE_RANGE]); - check(pattern!("{second}"), Some("00"), vec![SECOND_RANGE]); - check( - pattern!("{millisecond}"), - Some("000"), - vec![MILLISECOND_RANGE], - ); - check( - pattern!("{microsecond}"), - Some("000000"), - vec![MICROSECOND_RANGE], - ); - check( - pattern!("{nanosecond}"), - Some("000000000"), - vec![NANOSECOND_RANGE], - ); - check(pattern!("{am_pm}"), Some("{am_pm}"), vec![]); - check( - pattern!("{time_12}"), + check!("{month}", Some("00"), vec![MONTH_RANGE]); + check!("{day}", Some("00"), vec![DAY_RANGE]); + check!("{hour}", Some("00"), vec![HOUR_RANGE]); + check!("{hour_12}", Some("00"), vec![HOUR_12_RANGE]); + check!("{minute}", Some("00"), vec![MINUTE_RANGE]); + check!("{second}", Some("00"), vec![SECOND_RANGE]); + check!("{millisecond}", Some("000"), vec![MILLISECOND_RANGE]); + check!("{microsecond}", Some("000000"), vec![MICROSECOND_RANGE]); + check!("{nanosecond}", Some("000000000"), vec![NANOSECOND_RANGE]); + check!("{am_pm}", Some("{am_pm}"), vec![]); + check!( + "{time_12}", Some("00:00:00 {am_pm}"), vec![HOUR_12_RANGE, MINUTE_RANGE, SECOND_RANGE], ); - check( - pattern!("{time_short}"), + check!( + "{time_short}", Some("00:00"), vec![HOUR_RANGE, MINUTE_RANGE], ); - check( - pattern!("{time}"), + check!( + "{time}", Some("00:00:00"), vec![HOUR_RANGE, MINUTE_RANGE, SECOND_RANGE], ); - check( - pattern!("{tz_offset}"), + check!( + "{tz_offset}", Some("{begin_sign}00:00"), vec![HOUR_RANGE, MINUTE_RANGE], ); - check( - pattern!("{unix_timestamp}"), + check!( + "{unix_timestamp}", None as Option<&str>, vec![0..=i32::MAX as u64], ); cfg_if! { if #[cfg(feature = "source-location")] { - check( - pattern!("{full}"), + check!( + "{full}", Some(format!("[0000-00-00 00:00:00.000] [logger-name] [info] [pattern, {}:000] test payload", file!())), vec![ YEAR_RANGE, @@ -385,8 +410,8 @@ fn test_builtin_patterns() { ], ); } else { - check( - pattern!("{full}"), + check!( + "{full}", Some("[0000-00-00 00:00:00.000] [logger-name] [info] test payload"), vec![ YEAR_RANGE, @@ -401,30 +426,80 @@ fn test_builtin_patterns() { } } - check(pattern!("{level}"), Some("info"), vec![]); - check(pattern!("{level_short}"), Some("I"), vec![]); + check!("{level}", Some("info"), vec![]); + check!("{level_short}", Some("I"), vec![]); cfg_if! { if #[cfg(feature = "source-location")] { - check(pattern!("{source}"), Some(format!("{}:000", file!())), vec![SOURCE_RANGE]); - check(pattern!("{file_name}"), Some("pattern.rs"), vec![]); - check(pattern!("{file}"), Some(file!()), vec![]); - check(pattern!("{line}"), Some("000"), vec![SOURCE_RANGE]); - check(pattern!("{column}"), Some("0"), vec![SOURCE_RANGE]); - check(pattern!("{module_path}"), Some(module_path!()), vec![]); + check!("{source}", Some(format!("{}:000", file!())), vec![SOURCE_RANGE]); + check!("{file_name}", Some("pattern.rs"), vec![]); + check!("{file}", Some(file!()), vec![]); + check!("{line}", Some("000"), vec![SOURCE_RANGE]); + check!("{column}", Some("0"), vec![SOURCE_RANGE]); + check!("{module_path}", Some(module_path!()), vec![]); } else { - check(pattern!("{source}"), Some(""), vec![]); - check(pattern!("{file_name}"), Some(""), vec![]); - check(pattern!("{file}"), Some(""), vec![]); - check(pattern!("{line}"), Some(""), vec![]); - check(pattern!("{column}"), Some(""), vec![]); - check(pattern!("{module_path}"), Some(""), vec![]); + check!("{source}", Some(""), vec![]); + check!("{file_name}", Some(""), vec![]); + check!("{file}", Some(""), vec![]); + check!("{line}", Some(""), vec![]); + check!("{column}", Some(""), vec![]); + check!("{module_path}", Some(""), vec![]); } } - check(pattern!("{logger}"), Some("logger-name"), vec![]); - check(pattern!("{payload}"), Some("test payload"), vec![]); - check(pattern!("{pid}"), None as Option<&str>, vec![OS_ID_RANGE]); - check(pattern!("{tid}"), None as Option<&str>, vec![OS_ID_RANGE]); - check(pattern!("{eol}"), Some("{eol}"), vec![]); + check!("{logger}", Some("logger-name"), vec![]); + check!("{payload}", Some("test payload"), vec![]); + check!("{pid}", None as Option<&str>, vec![OS_ID_RANGE]); + check!("{tid}", None as Option<&str>, vec![OS_ID_RANGE]); + 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")]