diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d525dd..058acce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -589,6 +589,7 @@ Initial release ### [defmt-decoder-next] * [#958] Update to object 0.36 +* [#966] Add Frame::fragments() and Frame::display_fragments() ### [defmt-decoder-v1.0.0] (2025-04-01) @@ -947,6 +948,7 @@ Initial release --- +[#966]: https://github.com/knurling-rs/defmt/pull/966 [#968]: https://github.com/knurling-rs/defmt/pull/968 [#965]: https://github.com/knurling-rs/defmt/pull/965 [#960]: https://github.com/knurling-rs/defmt/pull/960 diff --git a/decoder/src/frame.rs b/decoder/src/frame.rs index b1aba9e2..88c7a13e 100644 --- a/decoder/src/frame.rs +++ b/decoder/src/frame.rs @@ -91,6 +91,52 @@ impl<'t> Frame<'t> { DisplayMessage { frame: self } } + /// Returns an iterator over the fragments of the message contained in this log frame. + /// + /// Collecting this into a String will yield the same result as [`Self::display_message`], but + /// this iterator will yield interpolated fragments on their own. For example, the log: + /// + /// ```ignore + /// defmt::info!("foo = {}, bar = {}", 1, 2); + /// ``` + /// + /// Will yield the following strings: + /// + /// ```ignore + /// vec!["foo = ", "1", ", bar = ", "2"] + /// ``` + /// + /// Note that nested fragments will not yield separately: + /// + /// ```ignore + /// defmt::info!("foo = {}", Foo { bar: 1 }); + /// ``` + /// + /// Will yield: + /// + /// ```ignore + /// vec!["foo = ", "Foo { bar: 1 }"] + /// ``` + /// + /// This iterator yields the same fragments as [`Self::fragments`], so you can zip them + /// together to get both representations. + pub fn display_fragments(&'t self) -> DisplayFragments<'t> { + DisplayFragments { + frame: self, + iter: self.fragments().into_iter(), + } + } + + /// Returns the fragments of the message contained in this log frame. + /// + /// Each fragment represents a part of the log message. See [`Fragment`] for more details. + /// + /// This iterator yields the same fragments as [`Self::display_fragments`], so you can zip them + /// together to get both representations. + pub fn fragments(&'t self) -> Vec> { + defmt_parser::parse(self.format, ParserMode::ForwardsCompatible).unwrap() + } + pub fn level(&self) -> Option { self.level } @@ -100,119 +146,120 @@ impl<'t> Frame<'t> { } fn format_args(&self, format: &str, args: &[Arg], parent_hint: Option<&DisplayHint>) -> String { - self.format_args_real(format, args, parent_hint).unwrap() // cannot fail, we only write to a `String` + let params = defmt_parser::parse(format, ParserMode::ForwardsCompatible).unwrap(); + let mut buf = String::new(); + for param in params { + self.format_fragment(param, &mut buf, args, parent_hint) + .unwrap(); // cannot fail, we only write to a `String` + } + buf } - fn format_args_real( + fn format_fragment( &self, - format: &str, + param: Fragment<'_>, + buf: &mut String, args: &[Arg], parent_hint: Option<&DisplayHint>, - ) -> Result { - let params = defmt_parser::parse(format, ParserMode::ForwardsCompatible).unwrap(); - let mut buf = String::new(); - for param in params { - match param { - Fragment::Literal(lit) => { - buf.push_str(&lit); - } - Fragment::Parameter(param) => { - let hint = param.hint.as_ref().or(parent_hint); - - match &args[param.index] { - Arg::Bool(x) => write!(buf, "{x}")?, - Arg::F32(x) => write!(buf, "{}", ryu::Buffer::new().format(*x))?, - Arg::F64(x) => write!(buf, "{}", ryu::Buffer::new().format(*x))?, - Arg::Uxx(x) => { - match param.ty { - Type::BitField(range) => { - let left_zeroes = - mem::size_of::() * 8 - range.end as usize; - let right_zeroes = left_zeroes + range.start as usize; - // isolate the desired bitfields - let bitfields = (*x << left_zeroes) >> right_zeroes; - - if let Some(DisplayHint::Ascii) = hint { - let bstr = bitfields - .to_be_bytes() - .iter() - .skip(right_zeroes / 8) - .copied() - .collect::>(); - self.format_bytes(&bstr, hint, &mut buf)? - } else { - self.format_u128(bitfields, hint, &mut buf)?; - } + ) -> Result<(), fmt::Error> { + match param { + Fragment::Literal(lit) => { + buf.push_str(&lit); + } + Fragment::Parameter(param) => { + let hint = param.hint.as_ref().or(parent_hint); + + match &args[param.index] { + Arg::Bool(x) => write!(buf, "{x}")?, + Arg::F32(x) => write!(buf, "{}", ryu::Buffer::new().format(*x))?, + Arg::F64(x) => write!(buf, "{}", ryu::Buffer::new().format(*x))?, + Arg::Uxx(x) => { + match param.ty { + Type::BitField(range) => { + let left_zeroes = mem::size_of::() * 8 - range.end as usize; + let right_zeroes = left_zeroes + range.start as usize; + // isolate the desired bitfields + let bitfields = (*x << left_zeroes) >> right_zeroes; + + if let Some(DisplayHint::Ascii) = hint { + let bstr = bitfields + .to_be_bytes() + .iter() + .skip(right_zeroes / 8) + .copied() + .collect::>(); + self.format_bytes(&bstr, hint, buf)? + } else { + self.format_u128(bitfields, hint, buf)?; } - _ => match hint { - Some(DisplayHint::ISO8601(precision)) => { - self.format_iso8601(*x as u64, precision, &mut buf)? - } - Some(DisplayHint::Debug) => { - self.format_u128(*x, parent_hint, &mut buf)? - } - _ => self.format_u128(*x, hint, &mut buf)?, - }, } + _ => match hint { + Some(DisplayHint::ISO8601(precision)) => { + self.format_iso8601(*x as u64, precision, buf)? + } + Some(DisplayHint::Debug) => { + self.format_u128(*x, parent_hint, buf)? + } + _ => self.format_u128(*x, hint, buf)?, + }, } - Arg::Ixx(x) => self.format_i128(*x, param.ty, hint, &mut buf)?, - Arg::Str(x) | Arg::Preformatted(x) => self.format_str(x, hint, &mut buf)?, - Arg::IStr(x) => self.format_str(x, hint, &mut buf)?, - Arg::Format { format, args } => match parent_hint { - Some(DisplayHint::Ascii) => { - buf.push_str(&self.format_args(format, args, parent_hint)); - } - _ => buf.push_str(&self.format_args(format, args, hint)), - }, - Arg::FormatSequence { args } => { - for arg in args { - buf.push_str(&self.format_args("{=?}", &[arg.clone()], hint)) - } + } + Arg::Ixx(x) => self.format_i128(*x, param.ty, hint, buf)?, + Arg::Str(x) | Arg::Preformatted(x) => self.format_str(x, hint, buf)?, + Arg::IStr(x) => self.format_str(x, hint, buf)?, + Arg::Format { format, args } => match parent_hint { + Some(DisplayHint::Ascii) => { + buf.push_str(&self.format_args(format, args, parent_hint)); } - Arg::FormatSlice { elements } => { - match hint { - // Filter Ascii Hints, which contains u8 byte slices - Some(DisplayHint::Ascii) - if elements.iter().filter(|e| e.format == "{=u8}").count() - != 0 => - { - let vals = elements - .iter() - .map(|e| match e.args.as_slice() { - [Arg::Uxx(v)] => u8::try_from(*v) - .expect("the value must be in u8 range"), - _ => panic!( - "FormatSlice should only contain one argument" - ), - }) - .collect::>(); - self.format_bytes(&vals, hint, &mut buf)? - } - _ => { - buf.write_str("[")?; - let mut is_first = true; - for element in elements { - if !is_first { - buf.write_str(", ")?; + _ => buf.push_str(&self.format_args(format, args, hint)), + }, + Arg::FormatSequence { args } => { + for arg in args { + buf.push_str(&self.format_args("{=?}", &[arg.clone()], hint)) + } + } + Arg::FormatSlice { elements } => { + match hint { + // Filter Ascii Hints, which contains u8 byte slices + Some(DisplayHint::Ascii) + if elements.iter().filter(|e| e.format == "{=u8}").count() != 0 => + { + let vals = elements + .iter() + .map(|e| match e.args.as_slice() { + [Arg::Uxx(v)] => { + u8::try_from(*v).expect("the value must be in u8 range") } - is_first = false; - buf.write_str(&self.format_args( - element.format, - &element.args, - hint, - ))?; + _ => panic!("FormatSlice should only contain one argument"), + }) + .collect::>(); + self.format_bytes(&vals, hint, buf)? + } + _ => { + buf.write_str("[")?; + let mut is_first = true; + for element in elements { + if !is_first { + buf.write_str(", ")?; } - buf.write_str("]")?; + is_first = false; + buf.write_str(&self.format_args( + element.format, + &element.args, + hint, + ))?; } + buf.write_str("]")?; } } - Arg::Slice(x) => self.format_bytes(x, hint, &mut buf)?, - Arg::Char(c) => write!(buf, "{c}")?, } + Arg::Slice(x) => self.format_bytes(x, hint, buf)?, + Arg::Char(c) => write!(buf, "{c}")?, } } } - Ok(buf) + + Ok(()) } fn format_u128( @@ -531,6 +578,26 @@ impl fmt::Display for DisplayMessage<'_> { } } +/// An iterator over the fragments of a log message, formatted as strings. +/// +/// See [`Frame::display_fragments`]. +pub struct DisplayFragments<'t> { + frame: &'t Frame<'t>, + iter: std::vec::IntoIter>, +} + +impl Iterator for DisplayFragments<'_> { + type Item = String; + + fn next(&mut self) -> Option { + let mut buf = String::new(); + self.frame + .format_fragment(self.iter.next()?, &mut buf, &self.frame.args, None) + .ok()?; + Some(buf) + } +} + /// Prints a `Frame` when formatted via `fmt::Display`, including all included metadata (level, /// timestamp, ...). pub struct DisplayFrame<'t> { diff --git a/decoder/src/lib.rs b/decoder/src/lib.rs index 5ec7f71a..dc63bda0 100644 --- a/decoder/src/lib.rs +++ b/decoder/src/lib.rs @@ -676,6 +676,47 @@ mod tests { ); } + #[test] + fn display_message() { + let entries = vec![ + TableEntry::new_without_symbol(Tag::Info, "x={=?}".to_owned()), + TableEntry::new_without_symbol(Tag::Derived, "Foo {{ x: {=u8} }}".to_owned()), + ]; + + let table = test_table(entries); + + let bytes = [ + 0, 0, // index + 1, 0, // index of the struct + 42, // Foo.x + ]; + + let frame = table.decode(&bytes).unwrap().0; + assert_eq!(frame.display_message().to_string(), "x=Foo { x: 42 }"); + } + + #[test] + fn display_fragments() { + let entries = vec![ + TableEntry::new_without_symbol(Tag::Info, "x={=?}".to_owned()), + TableEntry::new_without_symbol(Tag::Derived, "Foo {{ x: {=u8} }}".to_owned()), + ]; + + let table = test_table(entries); + + let bytes = [ + 0, 0, // index + 1, 0, // index of the struct + 42, // Foo.x + ]; + + let frame = table.decode(&bytes).unwrap().0; + assert_eq!( + frame.display_fragments().collect::>(), + ["x=", "Foo { x: 42 }"], + ); + } + #[test] fn display_i16_with_hex_hint() { // defmt::info!("x: {=i16:#x},y: {=i16:#x},z: {=i16:#x}", -1_i16, -100_i16, -1000_i16); @@ -715,6 +756,10 @@ mod tests { frame.display(false).to_string(), "0.000002 INFO x=S { x: 2a }", ); + assert_eq!( + frame.display_fragments().collect::>(), + ["x=", "S { x: 2a }"], + ); } #[test] @@ -738,6 +783,10 @@ mod tests { frame.display(false).to_string(), "0.000002 INFO x=S { x: 101010 }", ); + assert_eq!( + frame.display_fragments().collect::>(), + ["x=", "S { x: 101010 }"], + ); } #[test] @@ -761,6 +810,10 @@ mod tests { frame.display(false).to_string(), "0.000002 INFO S { x: \"Hello\" }", ); + assert_eq!( + frame.display_fragments().collect::>(), + ["S { x: \"Hello\" }"], + ); } #[test] @@ -790,6 +843,10 @@ mod tests { frame.display(false).to_string(), "0.000002 INFO [Data { name: b\"Hi\" }]", ); + assert_eq!( + frame.display_fragments().collect::>(), + ["[Data { name: b\"Hi\" }]"], + ); } #[test] @@ -1112,6 +1169,11 @@ mod tests { let frame = table.decode(&bytes).unwrap().0; assert_eq!(frame.display(false).to_string(), "0.000000 INFO x=Some(42)"); + assert_eq!(frame.display_message().to_string(), "x=Some(42)"); + assert_eq!( + frame.display_fragments().collect::>(), + ["x=", "Some(42)"], + ); let bytes = [ 4, 0, // string index (INFO) @@ -1122,5 +1184,10 @@ mod tests { let frame = table.decode(&bytes).unwrap().0; assert_eq!(frame.display(false).to_string(), "0.000001 INFO x=None"); + assert_eq!(frame.display_message().to_string(), "x=None"); + assert_eq!( + frame.display_fragments().collect::>(), + ["x=", "None"], + ); } }