1use defmt_parser::Level;
2#[cfg(not(test))]
3use proc_macro_error2::abort_call_site as panic;
4use std::fmt;
5use syn::Ident;
6
7// None = "off" pseudo-level
8pub(crate) type LogLevelOrOff = Option<Level>;
9
10// NOTE this is simpler than `syn::Path`; we do not want to accept e.g. `Vec::<Ty>::new`
11#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub(crate) struct ModulePath {
13 segments: Vec<String>,
14}
15
16/// Parses the contents of the `DEFMT_LOG` env var
17pub(crate) fn defmt_log(input: &str) -> impl Iterator<Item = Entry> + '_ {
18 input
19 .rsplit(',')
20 .filter(|entry| !entry.is_empty())
21 .map(|entry| {
22 if let Some((path, log_level)) = entry.rsplit_once('=') {
23 let module_path = ModulePath::parse(path);
24 let log_level = parse_log_level(log_level).unwrap_or_else(|_| {
25 panic!(
26 "unknown log level `{}` in DEFMT_LOG env var. \
27 expected one of: off, error, info, warn, debug, trace",
28 log_level
29 )
30 });
31
32 Entry::ModulePathLogLevel {
33 module_path,
34 log_level,
35 }
36 } else if let Ok(log_level) = parse_log_level(entry) {
37 Entry::LogLevel(log_level)
38 } else {
39 Entry::ModulePath(ModulePath::parse(entry))
40 }
41 })
42}
43
44#[derive(Debug, PartialEq)]
45pub(crate) enum Entry {
46 LogLevel(LogLevelOrOff),
47 ModulePath(ModulePath),
48 ModulePathLogLevel {
49 module_path: ModulePath,
50 log_level: LogLevelOrOff,
51 },
52}
53
54impl ModulePath {
55 pub(crate) fn from_crate_name(input: &str) -> Self {
56 if input.is_empty() && input.contains("::") {
57 panic!(
58 "DEFMT_LOG env var: crate name cannot be an empty string or contain path separators"
59 )
60 }
61 Self::parse(input)
62 }
63
64 pub(super) fn parse(input: &str) -> Self {
65 if input.is_empty() {
66 panic!("DEFMT_LOG env var: module path cannot be an empty string")
67 }
68
69 input.split("::").for_each(validate_identifier);
70
71 Self {
72 segments: input
73 .split("::")
74 .map(|segment| segment.to_string())
75 .collect(),
76 }
77 }
78
79 pub(super) fn crate_name(&self) -> &str {
80 &self.segments[0]
81 }
82}
83
84impl fmt::Display for ModulePath {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "{}", self.segments.join("::"))
87 }
88}
89
90fn parse_log_level(input: &str) -> Result<LogLevelOrOff, ()> {
91 Ok(Some(match input {
92 "debug" => Level::Debug,
93 "error" => Level::Error,
94 "info" => Level::Info,
95 "off" => return Ok(None),
96 "trace" => Level::Trace,
97 "warn" => Level::Warn,
98 _ => return Err(()),
99 }))
100}
101
102fn validate_identifier(input: &str) {
103 syn::parse_str::<Ident>(input)
104 .unwrap_or_else(|_| panic!("`{input}` is not a valid identifier"));
105}
106
107#[cfg(test)]
108mod tests {
109 use pretty_assertions::assert_eq;
110 use rstest::rstest;
111
112 use super::*;
113
114 #[test]
115 fn parses_from_the_right() {
116 let entries = defmt_log("krate=info,krate,info").collect::<Vec<_>>();
117 assert_eq!(
118 [
119 Entry::LogLevel(Some(Level::Info)),
120 Entry::ModulePath(ModulePath {
121 segments: vec!["krate".to_string()]
122 }),
123 Entry::ModulePathLogLevel {
124 module_path: ModulePath {
125 segments: vec!["krate".to_string()]
126 },
127 log_level: Some(Level::Info)
128 },
129 ],
130 entries.as_slice()
131 );
132 }
133
134 #[test]
135 fn after_sorting_innermost_modules_appear_last() {
136 let mut paths = [
137 ModulePath::parse("krate::module::inner"),
138 ModulePath::parse("krate"),
139 ModulePath::parse("krate::module"),
140 ];
141 paths.sort();
142
143 let expected = [
144 ModulePath::parse("krate"),
145 ModulePath::parse("krate::module"),
146 ModulePath::parse("krate::module::inner"),
147 ];
148 assert_eq!(expected, paths);
149 }
150
151 #[test]
152 fn accepts_raw_identifier() {
153 ModulePath::parse("krate::r#mod");
154 }
155
156 #[rstest]
157 #[case::has_module("krate::module")]
158 #[case::no_module("krate")]
159 fn modpath_crate_name(#[case] input: &str) {
160 let modpath = ModulePath::parse(input);
161 assert_eq!("krate", modpath.crate_name());
162 }
163
164 #[rstest]
165 #[case::crate_name_is_invalid("some-crate::module")]
166 #[case::module_name_is_invalid("krate::some-module")]
167 #[case::with_level("krate::some-module=info")]
168 #[should_panic = "not a valid identifier"]
169 fn rejects_invalid_identifier(#[case] input: &str) {
170 defmt_log(input).next();
171 }
172
173 #[test]
174 #[should_panic = "unknown log level"]
175 fn rejects_unknown_log_level() {
176 defmt_log("krate=module").next();
177 }
178
179 #[test]
180 #[should_panic = "module path cannot be an empty string"]
181 fn rejects_empty_module_path() {
182 defmt_log("=info").next();
183 }
184}
185