diff --git a/.vscode/settings.json b/.vscode/settings.json index 7691c50e..e38f9892 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -104,6 +104,9 @@ 150 ] }, + "rust-analyzer.cargo.features": [ + "qm" + ], "rust-analyzer.cargo.cfgs": [ "!miri" ], diff --git a/Cargo.lock b/Cargo.lock index 6ea2d488..232a617a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,5 +7,5 @@ name = "containers" version = "0.1.0" [[package]] -name = "log" +name = "mw_log_fmt" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 34c94b2b..22653f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,15 +2,9 @@ resolver = "2" # Split to default members without tests and examples. # Used when executing cargo from project root. -default-members = [ - "src/containers", - "src/log" -] +default-members = ["src/containers", "src/log/mw_log_fmt"] # Include tests and examples as a member for IDE support and Bazel builds. -members = [ - "src/containers", - "src/log" -] +members = ["src/containers", "src/log/mw_log_fmt"] [workspace.package] @@ -21,6 +15,7 @@ authors = ["S-CORE Contributors"] [workspace.dependencies] +mw_log_fmt = { path = "src/log/mw_log_fmt" } [workspace.lints.clippy] @@ -30,3 +25,4 @@ alloc_instead_of_core = "warn" [workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] } +missing_docs = "warn" diff --git a/docs/module/log/architecture/_assets/interface.puml b/docs/module/log/architecture/_assets/interface.puml index 3ed8deb2..a797f53c 100644 --- a/docs/module/log/architecture/_assets/interface.puml +++ b/docs/module/log/architecture/_assets/interface.puml @@ -47,20 +47,12 @@ package log <> { +fmt(&self, f: &mut dyn ScoreWrite, spec: &FormatSpec) : Result } - +interface ScoreDisplay <> { - +fmt(&self, f: &mut dyn ScoreWrite, spec: &FormatSpec) : Result - } - class mw_log_fmt <> { +write(output: &mut dyn ScoreWrite, args: Arguments<'_>) : Result +score_write!(format_string: &str, args...) : Result +score_writeln!(format_string: &str, args...) : Result } - - ScoreDebug -[hidden]down- ScoreDisplay - ScoreDisplay -[hidden]down- mw_log_fmt - } mw_log -- Level @@ -69,6 +61,4 @@ package log <> { mw_log -right- mw_log_fmt } - - @enduml diff --git a/docs/module/log/detailed_design/_assets/class_diagram.puml b/docs/module/log/detailed_design/_assets/class_diagram.puml index 60551fb0..cf35dd2c 100644 --- a/docs/module/log/detailed_design/_assets/class_diagram.puml +++ b/docs/module/log/detailed_design/_assets/class_diagram.puml @@ -161,8 +161,7 @@ package "mw_log_fmt crate" { -spec: FormatSpec -_lifetime: PhantomData<&'a ()> - +new_debug(value: &ScoreDebug, spec: FormatSpec) : Self - +new_display(value: &ScoreDisplay, spec: FormatSpec) : Self + +new(value: &ScoreDebug, spec: FormatSpec) : Self +fmt(&self, f: &mut dyn ScoreWrite, spec: &FormatSpec) : Result } @@ -186,17 +185,11 @@ package "mw_log_fmt crate" { +fmt(&self, f: &mut dyn ScoreWrite, spec: &FormatSpec) : Result } - +interface ScoreDisplay <> { - +fmt(&self, f: &mut dyn ScoreWrite, spec: &FormatSpec) : Result - } - - ' Placeholders are dependent on implementations provided by "Score*" traits. + ' Placeholders are dependent on "ScoreDebug" trait implementations. Placeholder --> ScoreDebug - Placeholder --> ScoreDisplay - ' All trait implementations rely on "ScoreWrite". + ' Trait implementations rely on "ScoreWrite". ScoreDebug --> ScoreWrite - ScoreDisplay --> ScoreWrite class mw_log_fmt <> { +write(output: &mut dyn ScoreWrite, args: Arguments<'_>) : Result diff --git a/docs/module/log/detailed_design/_assets/log_op.puml b/docs/module/log/detailed_design/_assets/log_op.puml index 557e58f2..80fcb378 100644 --- a/docs/module/log/detailed_design/_assets/log_op.puml +++ b/docs/module/log/detailed_design/_assets/log_op.puml @@ -17,7 +17,6 @@ end box box #LightYellow participant "mw_log_fmt\n<>" as mw_log_fmt -participant "ConcreteType\n<>" as score_display participant "ConcreteType\n<>" as score_debug end box @@ -54,13 +53,9 @@ else log-level-check-passed mw_log_fmt -> writer : write_str() writer --> mw_log_fmt else is-placeholder - alt display-requested - mw_log_fmt -> score_display : fmt() - score_display --> mw_log_fmt - else debug-requested - mw_log_fmt -> score_debug : fmt() - score_debug --> mw_log_fmt - end + mw_log_fmt -> score_debug : fmt() + score_debug --> mw_log_fmt + mw_log_fmt -> writer : write_() writer --> mw_log_fmt end diff --git a/src/log/BUILD b/src/log/BUILD index aa991bd5..e69de29b 100644 --- a/src/log/BUILD +++ b/src/log/BUILD @@ -1,24 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") - -rust_library( - name = "log", - srcs = glob(["src/**/*.rs"]), - visibility = ["//visibility:public"], -) - -rust_test( - name = "log_tests", - crate = ":log", -) diff --git a/src/log/Cargo.toml b/src/log/Cargo.toml deleted file mode 100644 index b5403bb7..00000000 --- a/src/log/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "log" -version.workspace = true -edition.workspace = true -license-file.workspace = true -authors.workspace = true - -[dependencies] - -[lints] -workspace = true diff --git a/src/log/mw_log_fmt/BUILD b/src/log/mw_log_fmt/BUILD new file mode 100644 index 00000000..764b3efa --- /dev/null +++ b/src/log/mw_log_fmt/BUILD @@ -0,0 +1,29 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "mw_log_fmt", + srcs = glob(["**/*.rs"]), + # TODO: expose required interface through `mw_log`. + visibility = ["//visibility:public"], +) + +rust_test( + name = "tests", + crate = "mw_log_fmt", + tags = [ + "unit_tests", + "ut", + ], +) diff --git a/src/log/mw_log_fmt/Cargo.toml b/src/log/mw_log_fmt/Cargo.toml new file mode 100644 index 00000000..67771301 --- /dev/null +++ b/src/log/mw_log_fmt/Cargo.toml @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +[package] +name = "mw_log_fmt" +version.workspace = true +authors.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +path = "lib.rs" + +[features] +qm = [] + +[lints] +workspace = true diff --git a/src/log/mw_log_fmt/builders.rs b/src/log/mw_log_fmt/builders.rs new file mode 100644 index 00000000..66f1bf8f --- /dev/null +++ b/src/log/mw_log_fmt/builders.rs @@ -0,0 +1,760 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Implementations of [`ScoreDebug`] implementation helper builders. + +use crate::{FormatSpec, Result, ScoreDebug, Writer}; + +/// Output a formatted struct. +/// +/// Useful as a part of [`ScoreDebug::fmt`] implementation. +#[must_use = "must eventually call `finish()` on ScoreDebug builders"] +pub struct DebugStruct<'a> { + writer: Writer<'a>, + spec: &'a FormatSpec, + result: Result, + has_fields: bool, +} + +impl<'a> DebugStruct<'a> { + /// Create `DebugStruct` instance. + pub fn new(writer: Writer<'a>, spec: &'a FormatSpec, name: &str) -> Self { + let result = writer.write_str(name, &FormatSpec::new()); + DebugStruct { + writer, + spec, + result, + has_fields: false, + } + } + + /// Adds a new field to the generated struct output. + pub fn field(&mut self, name: &str, value: &dyn ScoreDebug) -> &mut Self { + self.field_with(name, |f| value.fmt(f, self.spec)) + } + + /// Adds a new field to the generated struct output. + /// + /// This method is equivalent to [`DebugStruct::field`], but formats the value using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn field_with(&mut self, name: &str, value_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.result = self.result.and_then(|_| { + let prefix = if self.has_fields { ", " } else { " { " }; + let empty_spec = FormatSpec::new(); + self.writer.write_str(prefix, &empty_spec)?; + self.writer.write_str(name, &empty_spec)?; + self.writer.write_str(": ", &empty_spec)?; + value_fmt(self.writer) + }); + + self.has_fields = true; + self + } + + /// Marks the struct as non-exhaustive, indicating to the reader that there are some other fields that are not shown in the debug representation. + pub fn finish_non_exhaustive(&mut self) -> Result { + self.result = self.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.has_fields { + self.writer.write_str(", .. }", &empty_spec) + } else { + self.writer.write_str(" { .. }", &empty_spec) + } + }); + self.result + } + + /// Finishes output and returns any error encountered. + pub fn finish(&mut self) -> Result { + if self.has_fields { + let empty_spec = FormatSpec::new(); + self.result = self.result.and_then(|_| self.writer.write_str(" }", &empty_spec)); + } + self.result + } +} + +/// Output a formatted tuple. +/// +/// Useful as a part of [`ScoreDebug::fmt`] implementation. +#[must_use = "must eventually call `finish()` on ScoreDebug builders"] +pub struct DebugTuple<'a> { + writer: Writer<'a>, + spec: &'a FormatSpec, + result: Result, + fields: usize, + empty_name: bool, +} + +impl<'a> DebugTuple<'a> { + /// Create `DebugTuple` instance. + pub fn new(writer: Writer<'a>, spec: &'a FormatSpec, name: &str) -> Self { + let result = writer.write_str(name, &FormatSpec::new()); + DebugTuple { + writer, + spec, + result, + fields: 0, + empty_name: name.is_empty(), + } + } + + /// Adds a new field to the generated tuple struct output. + pub fn field(&mut self, value: &dyn ScoreDebug) -> &mut Self { + self.field_with(|f| value.fmt(f, self.spec)) + } + + /// Adds a new field to the generated tuple struct output. + /// + /// This method is equivalent to [`DebugTuple::field`], but formats the value using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn field_with(&mut self, value_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.result = self.result.and_then(|_| { + let prefix = if self.fields == 0 { "(" } else { ", " }; + let empty_spec = FormatSpec::new(); + self.writer.write_str(prefix, &empty_spec)?; + value_fmt(self.writer) + }); + + self.fields += 1; + self + } + + /// Marks the tuple struct as non-exhaustive, indicating to the reader that there are some other fields that are not shown in the debug representation. + pub fn finish_non_exhaustive(&mut self) -> Result { + self.result = self.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.fields > 0 { + self.writer.write_str(", ..)", &empty_spec) + } else { + self.writer.write_str("(..)", &empty_spec) + } + }); + self.result + } + + /// Finishes output and returns any error encountered. + pub fn finish(&mut self) -> Result { + if self.fields > 0 { + self.result = self.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.fields == 1 && self.empty_name { + self.writer.write_str(",", &empty_spec)?; + } + self.writer.write_str(")", &empty_spec) + }); + } + self.result + } +} + +/// A helper used to print list-like items with no special formatting. +struct DebugInner<'a> { + writer: Writer<'a>, + spec: &'a FormatSpec, + result: Result, + has_fields: bool, +} + +impl<'a> DebugInner<'a> { + fn entry_with(&mut self, entry_writer: F) + where + F: FnOnce(Writer) -> Result, + { + self.result = self.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.has_fields { + self.writer.write_str(", ", &empty_spec)? + } + entry_writer(self.writer) + }); + + self.has_fields = true; + } +} + +/// Output a formatted set of items. +/// +/// Useful as a part of [`ScoreDebug::fmt`] implementation. +#[must_use = "must eventually call `finish()` on ScoreDebug builders"] +pub struct DebugSet<'a> { + inner: DebugInner<'a>, +} + +impl<'a> DebugSet<'a> { + /// Create `DebugSet` instance. + pub fn new(writer: Writer<'a>, spec: &'a FormatSpec) -> Self { + let result = writer.write_str("{", &FormatSpec::new()); + DebugSet { + inner: DebugInner { + writer, + spec, + result, + has_fields: false, + }, + } + } + + /// Adds a new entry to the set output. + pub fn entry(&mut self, entry: &dyn ScoreDebug) -> &mut Self { + self.inner.entry_with(|f| entry.fmt(f, self.inner.spec)); + self + } + + /// Adds a new entry to the set output. + /// + /// This method is equivalent to [`DebugSet::entry`], but formats the entry using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn entry_with(&mut self, entry_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.inner.entry_with(entry_fmt); + self + } + + /// Adds the contents of an iterator of entries to the set output. + pub fn entries(&mut self, entries: I) -> &mut Self + where + D: ScoreDebug, + I: IntoIterator, + { + for entry in entries { + self.entry(&entry); + } + self + } + + /// Marks the set as non-exhaustive, indicating to the reader that there are some other elements that are not shown in the debug representation. + pub fn finish_non_exhaustive(&mut self) -> Result { + self.inner.result = self.inner.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.inner.has_fields { + self.inner.writer.write_str(", ..}", &empty_spec) + } else { + self.inner.writer.write_str("..}", &empty_spec) + } + }); + self.inner.result + } + + /// Finishes output and returns any error encountered. + pub fn finish(&mut self) -> Result { + self.inner.result = self.inner.result.and_then(|_| self.inner.writer.write_str("}", &FormatSpec::new())); + self.inner.result + } +} + +/// Output a formatted list of items. +/// +/// Useful as a part of [`ScoreDebug::fmt`] implementation. +#[must_use = "must eventually call `finish()` on ScoreDebug builders"] +pub struct DebugList<'a> { + inner: DebugInner<'a>, +} + +impl<'a> DebugList<'a> { + /// Create `DebugList` instance. + pub fn new(writer: Writer<'a>, spec: &'a FormatSpec) -> Self { + let result = writer.write_str("[", &FormatSpec::new()); + DebugList { + inner: DebugInner { + writer, + spec, + result, + has_fields: false, + }, + } + } + + /// Adds a new entry to the list output. + pub fn entry(&mut self, entry: &dyn ScoreDebug) -> &mut Self { + self.inner.entry_with(|f| entry.fmt(f, self.inner.spec)); + self + } + + /// Adds a new entry to the list output. + /// + /// This method is equivalent to [`DebugList::entry`], but formats the entry using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn entry_with(&mut self, entry_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.inner.entry_with(entry_fmt); + self + } + + /// Adds the contents of an iterator of entries to the list output. + pub fn entries(&mut self, entries: I) -> &mut Self + where + D: ScoreDebug, + I: IntoIterator, + { + for entry in entries { + self.entry(&entry); + } + self + } + + /// Marks the list as non-exhaustive, indicating to the reader that there are some other elements that are not shown in the debug representation. + pub fn finish_non_exhaustive(&mut self) -> Result { + self.inner.result.and_then(|_| { + let empty_spec = FormatSpec::new(); + if self.inner.has_fields { + self.inner.writer.write_str(", ..]", &empty_spec) + } else { + self.inner.writer.write_str("..]", &empty_spec) + } + }) + } + + /// Finishes output and returns any error encountered. + pub fn finish(&mut self) -> Result { + self.inner.result = self.inner.result.and_then(|_| self.inner.writer.write_str("]", &FormatSpec::new())); + self.inner.result + } +} + +/// Output a formatted map of items. +/// +/// Useful as a part of [`ScoreDebug::fmt`] implementation. +#[must_use = "must eventually call `finish()` on ScoreDebug builders"] +pub struct DebugMap<'a> { + writer: Writer<'a>, + spec: &'a FormatSpec, + result: Result, + has_fields: bool, + has_key: bool, +} + +impl<'a> DebugMap<'a> { + /// Create `DebugMap` instance. + pub fn new(writer: Writer<'a>, spec: &'a FormatSpec) -> Self { + let result = writer.write_str("{", &FormatSpec::new()); + DebugMap { + writer, + spec, + result, + has_fields: false, + has_key: false, + } + } + + /// Adds a new entry to the map output. + pub fn entry(&mut self, key: &dyn ScoreDebug, value: &dyn ScoreDebug) -> &mut Self { + self.key(key).value(value) + } + + /// Adds the key part of a new entry to the map output. + /// + /// This method, together with `value`, is an alternative to `entry` that can be used when the complete entry isn't known upfront. + /// Prefer the `entry` method when it's possible to use. + /// + /// # Panics + /// + /// `key` must be called before `value` and each call to `key` must be followed by a corresponding call to `value`. + /// Otherwise this method will panic. + pub fn key(&mut self, key: &dyn ScoreDebug) -> &mut Self { + self.key_with(|f| key.fmt(f, self.spec)) + } + + /// Adds the key part of a new entry to the map output. + /// + /// This method is equivalent to [`DebugMap::key`], but formats the key using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn key_with(&mut self, key_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.result = self.result.and_then(|_| { + assert!( + !self.has_key, + "attempted to begin a new map entry \ + without completing the previous one" + ); + + let empty_spec = FormatSpec::new(); + if self.has_fields { + self.writer.write_str(", ", &empty_spec)? + } + key_fmt(self.writer)?; + self.writer.write_str(": ", &empty_spec)?; + + self.has_key = true; + Ok(()) + }); + + self + } + + /// Adds the value part of a new entry to the map output. + /// + /// This method, together with `key`, is an alternative to `entry` that can be used when the complete entry isn't known upfront. + /// Prefer the `entry` method when it's possible to use. + /// + /// # Panics + /// + /// `key` must be called before `value` and each call to `key` must be followed by a corresponding call to `value`. + /// Otherwise this method will panic. + pub fn value(&mut self, value: &dyn ScoreDebug) -> &mut Self { + self.value_with(|f| value.fmt(f, self.spec)) + } + + /// Adds the value part of a new entry to the map output. + /// + /// This method is equivalent to [`DebugMap::value`], but formats the value using a provided closure rather than by calling [`ScoreDebug::fmt`]. + pub fn value_with(&mut self, value_fmt: F) -> &mut Self + where + F: FnOnce(Writer) -> Result, + { + self.result = self.result.and_then(|_| { + assert!(self.has_key, "attempted to format a map value before its key"); + value_fmt(self.writer)?; + self.has_key = false; + Ok(()) + }); + + self.has_fields = true; + self + } + + /// Adds the contents of an iterator of entries to the map output. + pub fn entries(&mut self, entries: I) -> &mut Self + where + K: ScoreDebug, + V: ScoreDebug, + I: IntoIterator, + { + for (k, v) in entries { + self.entry(&k, &v); + } + self + } + + /// Marks the map as non-exhaustive, indicating to the reader that there are some other entries that are not shown in the debug representation. + pub fn finish_non_exhaustive(&mut self) -> Result { + self.result = self.result.and_then(|_| { + assert!(!self.has_key, "attempted to finish a map with a partial entry"); + + let empty_spec = FormatSpec::new(); + if self.has_fields { + self.writer.write_str(", ..}", &empty_spec) + } else { + self.writer.write_str("..}", &empty_spec) + } + }); + self.result + } + + /// Finishes output and returns any error encountered. + /// + /// # Panics + /// + /// `key` must be called before `value` and each call to `key` must be followed by a corresponding call to `value`. + /// Otherwise this method will panic. + pub fn finish(&mut self) -> Result { + self.result = self.result.and_then(|_| { + assert!(!self.has_key, "attempted to finish a map with a partial entry"); + let empty_spec = FormatSpec::new(); + self.writer.write_str("}", &empty_spec) + }); + self.result + } +} + +#[cfg(test)] +mod tests { + use crate::builders::{DebugList, DebugMap, DebugSet, DebugStruct, DebugTuple}; + use crate::test_utils::StringWriter; + use crate::FormatSpec; + + #[test] + fn test_struct_finish_non_exhaustive() { + #[derive(Debug)] + struct Point { + x: i32, + y: i32, + } + + let v = Point { x: 123, y: 321 }; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugStruct::new(&mut writer, &spec, "Point") + .field("x", &v.x) + .field("y", &v.y) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "Point { x: 123, y: 321, .. }"); + } + + #[test] + fn test_struct_finish() { + #[derive(Debug)] + struct Point { + x: i32, + y: i32, + } + + let v = Point { x: 123, y: 321 }; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugStruct::new(&mut writer, &spec, "Point") + .field("x", &v.x) + .field("y", &v.y) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_struct_empty_finish_non_exhaustive() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugStruct::new(&mut writer, &spec, "X") + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "X { .. }"); + } + + #[test] + fn test_struct_empty_finish() { + #[derive(Debug)] + struct X; + + let v = X; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugStruct::new(&mut writer, &spec, "X").finish().map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_tuple_finish_non_exhaustive() { + let v = (123, 456, 789); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugTuple::new(&mut writer, &spec, "") + .field(&v.0) + .field(&v.1) + .field(&v.2) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "(123, 456, 789, ..)"); + } + + #[test] + fn test_tuple_empty_non_exhaustive() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugTuple::new(&mut writer, &spec, "") + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "(..)"); + } + + #[test] + fn test_tuple_finish() { + let v = (123, 456, 789); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugTuple::new(&mut writer, &spec, "") + .field(&v.0) + .field(&v.1) + .field(&v.2) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_tuple_empty_finish() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugTuple::new(&mut writer, &spec, "").finish().map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), ""); + } + + #[test] + fn test_tuple_single_finish() { + let v = (531,); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugTuple::new(&mut writer, &spec, "") + .field(&v.0) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_set_finish_non_exhaustive() { + let v = std::collections::BTreeSet::from([123, 456, 789]); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugSet::new(&mut writer, &spec) + .entries(v.clone()) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "{123, 456, 789, ..}"); + } + + #[test] + fn test_set_empty_finish_non_exhaustive() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugSet::new(&mut writer, &spec) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "{..}"); + } + + #[test] + fn test_set_finish() { + let v = std::collections::HashSet::from([123, 456, 789]); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugSet::new(&mut writer, &spec) + .entries(v.clone()) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_set_empty_finish() { + let v = std::collections::HashSet::::new(); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugSet::new(&mut writer, &spec) + .entries(v.clone()) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_list_finish_non_exhaustive() { + let v = [123, 456, 789]; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugList::new(&mut writer, &spec) + .entries(v) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "[123, 456, 789, ..]"); + } + + #[test] + fn test_list_empty_finish_non_exhaustive() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugList::new(&mut writer, &spec) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "[..]"); + } + + #[test] + fn test_list_finish() { + let v = [123, 456, 789]; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugList::new(&mut writer, &spec) + .entries(v) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_list_empty_finish() { + let v: [i32; 0] = []; + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugList::new(&mut writer, &spec) + .entries(v) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } + + #[test] + fn test_map_finish_non_exhaustive() { + let v = std::collections::BTreeMap::from([("first", 123), ("second", 456), ("third", 789)]); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugMap::new(&mut writer, &spec) + .entries(v.clone()) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "{\"first\": 123, \"second\": 456, \"third\": 789, ..}"); + } + + #[test] + fn test_map_empty_finish_non_exhaustive() { + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugMap::new(&mut writer, &spec) + .finish_non_exhaustive() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), "{..}"); + } + + #[test] + fn test_map_empty_finish() { + let v = std::collections::BTreeMap::<&str, i32>::new(); + + let mut writer = StringWriter::new(); + let spec = FormatSpec::new(); + let _ = DebugMap::new(&mut writer, &spec) + .entries(v.clone()) + .finish() + .map_err(|_| panic!("failed to finish")); + + assert_eq!(writer.get(), format!("{:?}", v)); + } +} diff --git a/src/log/mw_log_fmt/fmt.rs b/src/log/mw_log_fmt/fmt.rs new file mode 100644 index 00000000..60f3a395 --- /dev/null +++ b/src/log/mw_log_fmt/fmt.rs @@ -0,0 +1,221 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::FormatSpec; +use core::marker::PhantomData; +use core::ptr::NonNull; + +/// The type returned by writer methods. +pub type Result = core::result::Result<(), Error>; + +/// The type of the writer. +pub type Writer<'a> = &'a mut dyn ScoreWrite; + +/// The error type which is returned from writing a message. +/// +/// This type does not support transmission of an error other than an error occurred. +/// This is because, despite the existence of this error, writing is considered an infallible operation. +/// `fmt()` implementors should not return this `Error` unless the received it from their [`ScoreWrite`] implementation. +#[derive(Copy, Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Error; + +/// A trait for writing into message frames. +/// +/// This trait accepts multiple data types. +/// Implementation is responsible for output formatting based on provided spec. +pub trait ScoreWrite { + /// Write a `bool` into this writer. + fn write_bool(&mut self, v: &bool, spec: &FormatSpec) -> Result; + /// Write a `f32` into this writer. + fn write_f32(&mut self, v: &f32, spec: &FormatSpec) -> Result; + /// Write a `f64` into this writer. + fn write_f64(&mut self, v: &f64, spec: &FormatSpec) -> Result; + /// Write a `i8` into this writer. + fn write_i8(&mut self, v: &i8, spec: &FormatSpec) -> Result; + /// Write a `i16` into this writer. + fn write_i16(&mut self, v: &i16, spec: &FormatSpec) -> Result; + /// Write a `i32` into this writer. + fn write_i32(&mut self, v: &i32, spec: &FormatSpec) -> Result; + /// Write a `i64` into this writer. + fn write_i64(&mut self, v: &i64, spec: &FormatSpec) -> Result; + /// Write a `u8` into this writer. + fn write_u8(&mut self, v: &u8, spec: &FormatSpec) -> Result; + /// Write a `u16` into this writer. + fn write_u16(&mut self, v: &u16, spec: &FormatSpec) -> Result; + /// Write a `u32` into this writer. + fn write_u32(&mut self, v: &u32, spec: &FormatSpec) -> Result; + /// Write a `u64` into this writer. + fn write_u64(&mut self, v: &u64, spec: &FormatSpec) -> Result; + /// Write a `&str` into this writer. + fn write_str(&mut self, v: &str, spec: &FormatSpec) -> Result; +} + +/// Data placeholder in message. +pub struct Placeholder<'a> { + value: NonNull<()>, + formatter: fn(NonNull<()>, Writer, &FormatSpec) -> Result, + spec: FormatSpec, + _lifetime: PhantomData<&'a ()>, +} + +impl<'a> Placeholder<'a> { + /// Create the placeholder to be represented using `ScoreDebug`. + pub const fn new(value: &'a T, spec: FormatSpec) -> Self { + let value = NonNull::from_ref(value).cast(); + let formatter = |v: NonNull<()>, f: Writer, spec: &FormatSpec| { + // SAFETY: borrow checker will ensure that value won't be mutated for as long as the returned `Self` instance is alive. + let typed = unsafe { v.cast::().as_ref() }; + typed.fmt(f, spec) + }; + Self { + value, + formatter, + spec, + _lifetime: PhantomData, + } + } + + /// Get format spec of this placeholder. + pub fn format_spec(&self) -> &FormatSpec { + &self.spec + } + + /// Write requested representation of data to the provided writer. + pub fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + (self.formatter)(self.value, f, spec) + } +} + +/// Message fragment. +/// A string literal or data placeholder. +pub enum Fragment<'a> { + /// Fragment is a string literal, with no additional formatting. + Literal(&'a str), + /// Fragment is a placeholder for provided data. + Placeholder(Placeholder<'a>), +} + +/// Array of message parts. +/// Consists of [`Fragment`] entities. +#[derive(Copy, Clone)] +pub struct Arguments<'a>(pub &'a [Fragment<'a>]); + +impl ScoreDebug for Arguments<'_> { + fn fmt(&self, f: Writer, _spec: &FormatSpec) -> Result { + write(f, *self) + } +} + +/// `ScoreDebug` provides the output in a programmer-facing, debugging context. +/// Replacement for [`core::fmt::Debug`]. +pub trait ScoreDebug { + /// Write debug representation of `self` to the provided writer. + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result; +} + +/// Write [`Arguments`] into provided `output` writer. +/// +/// The arguments will be formatted according to provided format spec. +pub fn write(output: Writer, args: Arguments<'_>) -> Result { + for fragment in args.0 { + match fragment { + Fragment::Literal(s) => output.write_str(s, &FormatSpec::new()), + Fragment::Placeholder(ph) => ph.fmt(output, &ph.spec), + }?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::test_utils::StringWriter; + use crate::{write, Arguments, FormatSpec, Fragment, Placeholder, ScoreDebug}; + + #[test] + fn test_arguments_debug() { + let mut w = StringWriter::new(); + let fragments = [ + Fragment::Literal("test_"), + Fragment::Placeholder(Placeholder::new(&true, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123.4f32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&432.2f64, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-100i8, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-1234i16, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-123456i32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-1200000000000000000i64, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123u8, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&1234u16, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123456u32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&1200000000000000000u64, FormatSpec::new())), + Fragment::Literal("_string"), + ]; + let args = Arguments(&fragments); + + let result = ScoreDebug::fmt(&args, &mut w, &FormatSpec::new()); + assert!(result == Ok(())); + assert!(w.get() == "test_true123.4432.2-100-1234-123456-120000000000000000012312341234561200000000000000000_string") + } + + #[test] + fn test_write_empty() { + let mut w = StringWriter::new(); + let args = Arguments(&[]); + assert!(write(&mut w, args) == Ok(())); + } + + #[test] + fn test_write_literals_only() { + let mut w = StringWriter::new(); + let args = Arguments(&[Fragment::Literal("test_"), Fragment::Literal("string")]); + assert!(write(&mut w, args) == Ok(())); + assert!(w.get() == "test_string"); + } + + #[test] + fn test_write_placeholders_only() { + let mut w = StringWriter::new(); + let fragments = [ + Fragment::Placeholder(Placeholder::new(&true, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123.4f32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&432.2f64, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-100i8, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-1234i16, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-123456i32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&-1200000000000000000i64, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123u8, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&1234u16, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&123456u32, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&1200000000000000000u64, FormatSpec::new())), + Fragment::Placeholder(Placeholder::new(&"test", FormatSpec::new())), + ]; + let args = Arguments(&fragments); + assert!(write(&mut w, args) == Ok(())); + + let exp_pattern = "true123.4432.2-100-1234-123456-120000000000000000012312341234561200000000000000000\"test\""; + assert!(w.get() == exp_pattern); + } + + #[test] + fn test_write_mixed() { + let mut w = StringWriter::new(); + let fragments = [ + Fragment::Literal("test_"), + Fragment::Placeholder(Placeholder::new(&123i8, FormatSpec::new())), + Fragment::Literal("_string"), + ]; + let args = Arguments(&fragments); + assert!(write(&mut w, args) == Ok(())); + assert!(w.get() == "test_123_string"); + } +} diff --git a/src/log/mw_log_fmt/fmt_impl.rs b/src/log/mw_log_fmt/fmt_impl.rs new file mode 100644 index 00000000..169ab89f --- /dev/null +++ b/src/log/mw_log_fmt/fmt_impl.rs @@ -0,0 +1,225 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! `ScoreDebug` implementations for common types. + +use crate::builders::DebugList; +use crate::fmt::{Error, Result, ScoreDebug, Writer}; +use crate::fmt_spec::FormatSpec; + +macro_rules! impl_debug_for_t { + ($t:ty, $fn:ident) => { + impl ScoreDebug for $t { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + f.$fn(self, spec) + } + } + }; +} + +impl_debug_for_t!(bool, write_bool); +impl_debug_for_t!(f32, write_f32); +impl_debug_for_t!(f64, write_f64); +impl_debug_for_t!(i8, write_i8); +impl_debug_for_t!(i16, write_i16); +impl_debug_for_t!(i32, write_i32); +impl_debug_for_t!(i64, write_i64); +impl_debug_for_t!(u8, write_u8); +impl_debug_for_t!(u16, write_u16); +impl_debug_for_t!(u32, write_u32); +impl_debug_for_t!(u64, write_u64); + +impl ScoreDebug for str { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + let empty_spec = FormatSpec::new(); + f.write_str("\"", &empty_spec)?; + f.write_str(self, spec)?; + f.write_str("\"", &empty_spec) + } +} + +impl ScoreDebug for String { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&self.as_str(), f, spec) + } +} + +macro_rules! impl_debug_for_t_casted { + ($ti:ty, $to:ty, $fn:ident) => { + impl ScoreDebug for $ti { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + let casted = <$to>::try_from(*self).map_err(|_| Error)?; + f.$fn(&casted, spec) + } + } + }; +} + +#[cfg(target_pointer_width = "32")] +impl_debug_for_t_casted!(isize, i32, write_i32); +#[cfg(target_pointer_width = "64")] +impl_debug_for_t_casted!(isize, i64, write_i64); +#[cfg(target_pointer_width = "32")] +impl_debug_for_t_casted!(usize, u32, write_u32); +#[cfg(target_pointer_width = "64")] +impl_debug_for_t_casted!(usize, u64, write_u64); + +impl ScoreDebug for &T { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&**self, f, spec) + } +} + +impl ScoreDebug for &mut T { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&**self, f, spec) + } +} + +impl ScoreDebug for [T] { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + let mut debug_list = DebugList::new(f, spec); + debug_list.entries(self.iter()).finish() + } +} + +impl ScoreDebug for [T; N] { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&&self[..], f, spec) + } +} + +impl ScoreDebug for Vec { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&**self, f, spec) + } +} + +impl ScoreDebug for std::rc::Rc { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&**self, f, spec) + } +} + +impl ScoreDebug for std::sync::Arc { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(&**self, f, spec) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::common_test_debug; + + #[test] + fn test_bool_debug() { + common_test_debug(true); + } + + #[test] + fn test_f32_debug() { + common_test_debug(123.4f32); + } + + #[test] + fn test_f64_debug() { + common_test_debug(123.4f64); + } + + #[test] + fn test_i8_debug() { + common_test_debug(-123i8); + } + + #[test] + fn test_i16_debug() { + common_test_debug(-1234i16); + } + + #[test] + fn test_i32_debug() { + common_test_debug(-123456i32); + } + + #[test] + fn test_i64_debug() { + common_test_debug(-1200000000000000000i64); + } + + #[test] + fn test_u8_debug() { + common_test_debug(123u8); + } + + #[test] + fn test_u16_debug() { + common_test_debug(1234u16); + } + + #[test] + fn test_u32_debug() { + common_test_debug(123456u32); + } + + #[test] + fn test_u64_debug() { + common_test_debug(1200000000000000000u64); + } + + #[test] + fn test_str_debug() { + common_test_debug("test"); + } + + #[test] + fn test_string_debug() { + common_test_debug(String::from("test")); + } + + #[test] + fn test_isize_debug() { + common_test_debug(-1200000000000000000isize); + } + + #[test] + fn test_usize_debug() { + common_test_debug(1200000000000000000usize); + } + + #[test] + fn test_slice_debug() { + common_test_debug([123, 456, 789].as_slice()); + } + + #[test] + fn test_array_debug() { + common_test_debug([123, 456, 789]); + } + + #[test] + fn test_vec_debug() { + common_test_debug(vec![987, 654, 321, 159]); + } + + #[test] + fn test_rc_debug() { + let rc = std::rc::Rc::new(444); + common_test_debug(rc); + } + + #[test] + fn test_arc_debug() { + let arc = std::sync::Arc::new(654); + common_test_debug(arc); + } +} diff --git a/src/log/mw_log_fmt/fmt_impl_qm.rs b/src/log/mw_log_fmt/fmt_impl_qm.rs new file mode 100644 index 00000000..3ef12bf3 --- /dev/null +++ b/src/log/mw_log_fmt/fmt_impl_qm.rs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! `ScoreDebug` implementations for types that are not ASIL-B certified. + +use crate::fmt::{Result, ScoreDebug, Writer}; +use crate::fmt_spec::FormatSpec; +use std::path::{Path, PathBuf}; + +// TODO: replace with `core::char::MAX_LEN_UTF8` once stable. +const MAX_LEN_UTF8: usize = 4; + +impl ScoreDebug for Path { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + let enc_bytes = self.as_os_str().as_encoded_bytes(); + let utf8_chunks = enc_bytes.utf8_chunks(); + + for chunk in utf8_chunks { + let valid = chunk.valid(); + // If we successfully decoded the whole chunk as a valid string then + // we can return a direct formatting of the string which will also + // respect various formatting flags if possible. + if chunk.invalid().is_empty() { + return ScoreDebug::fmt(valid, f, spec); + } + + f.write_str(valid, spec)?; + f.write_str(core::char::REPLACEMENT_CHARACTER.encode_utf8(&mut [0; MAX_LEN_UTF8]), spec)?; + } + + Ok(()) + } +} + +impl ScoreDebug for PathBuf { + fn fmt(&self, f: Writer, spec: &FormatSpec) -> Result { + ScoreDebug::fmt(self.as_path(), f, spec) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::common_test_debug; + use std::path::{Path, PathBuf}; + + #[test] + fn test_path_ref_debug() { + common_test_debug(Path::new("/tmp/test_path")); + } + + #[test] + fn test_pathbuf_debug() { + common_test_debug(PathBuf::from("/tmp/test_path")); + } +} diff --git a/src/log/mw_log_fmt/fmt_spec.rs b/src/log/mw_log_fmt/fmt_spec.rs new file mode 100644 index 00000000..54beb132 --- /dev/null +++ b/src/log/mw_log_fmt/fmt_spec.rs @@ -0,0 +1,378 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Alignment of written data. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Alignment { + /// Align to left (`<`). + Left, + /// Align to right (`>`). + Right, + /// Align to center (`^`). + Center, +} + +/// Add sign character for numeric values. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Sign { + /// Always show sign (`+`). + Plus, + /// Unused (`-`). + Minus, +} + +/// Format integer values as hexadecimal for `ScoreDebug` implementations. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum DebugAsHex { + /// Format integer values to lower hex. + Lower, + /// Format integer values to upper hex. + Upper, +} + +/// Display data in a provided format. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum DisplayHint { + /// `{}` or `{:}`. + NoHint, + /// `{:?}`. + Debug, + /// `{:o}`. + Octal, + /// `{:x}`. + LowerHex, + /// `{:X}`. + UpperHex, + /// `{:p}`. + Pointer, + /// `{:b}`. + Binary, + /// `{:e}`. + LowerExp, + /// `{:E}`. + UpperExp, +} + +/// Format spec. +/// +/// format_spec := [[fill]align][sign]['#']['0'][width]['.' precision][type] +/// fill := character +/// align := '<' | '^' | '>' +/// sign := '+' | '-' +/// width := count +/// precision := count | '*' +/// type := '?' | 'x?' | 'X?' | 'o' | 'x' | 'X' | 'p' | 'b' | 'e' | 'E' +/// parameter := argument '$' +#[derive(Clone)] +pub struct FormatSpec { + display_hint: DisplayHint, + fill: char, + align: Option, + sign: Option, + alternate: bool, + zero_pad: bool, + debug_as_hex: Option, + width: Option, + precision: Option, +} + +impl FormatSpec { + /// Create format spec with default parameters. + /// + /// - `display_hint`: `DisplayHint::NoHint` + /// - `fill`: `' '` + /// - `align`: `None` + /// - `sign`: `None` + /// - `alternate`: `false` + /// - `zero_pad`: `false` + /// - `debug_as_hex`: `None` + /// - `width`: `None` + /// - `precision`: `None` + pub fn new() -> Self { + Self { + display_hint: DisplayHint::NoHint, + fill: ' ', + align: None, + sign: None, + alternate: false, + zero_pad: false, + debug_as_hex: None, + width: None, + precision: None, + } + } + + /// Create format spec with provided parameters. + #[allow(clippy::too_many_arguments)] + pub fn from_params( + display_hint: DisplayHint, + fill: char, + align: Option, + sign: Option, + alternate: bool, + zero_pad: bool, + debug_as_hex: Option, + width: Option, + precision: Option, + ) -> Self { + Self { + display_hint, + fill, + align, + sign, + alternate, + zero_pad, + debug_as_hex, + width, + precision, + } + } + + /// Set display hint. + pub fn display_hint(&mut self, display_hint: DisplayHint) -> &mut Self { + self.display_hint = display_hint; + self + } + + /// Set fill character. + pub fn fill(&mut self, fill: char) -> &mut Self { + self.fill = fill; + self + } + + /// Set alignment. + pub fn align(&mut self, align: Option) -> &mut Self { + self.align = align; + self + } + + /// Set sign. + pub fn sign(&mut self, sign: Option) -> &mut Self { + self.sign = sign; + self + } + + /// Set alternate formatting mode. + pub fn alternate(&mut self, alternate: bool) -> &mut Self { + self.alternate = alternate; + self + } + + /// Set zero padding mode. + pub fn zero_pad(&mut self, zero_pad: bool) -> &mut Self { + self.zero_pad = zero_pad; + self + } + + /// Set debug as hex mode. + pub fn debug_as_hex(&mut self, debug_as_hex: Option) -> &mut Self { + self.debug_as_hex = debug_as_hex; + self + } + + /// Set width. + pub fn width(&mut self, width: Option) -> &mut Self { + self.width = width; + self + } + + /// Set precision. + pub fn precision(&mut self, precision: Option) -> &mut Self { + self.precision = precision; + self + } + + /// Get display hint. + pub fn get_display_hint(&self) -> DisplayHint { + self.display_hint + } + + /// Get fill character. + pub fn get_fill(&self) -> char { + self.fill + } + + /// Get alignment. + pub fn get_align(&self) -> Option { + self.align + } + + /// Get sign. + pub fn get_sign(&self) -> Option { + self.sign + } + + /// Get alternate mode. + pub fn get_alternate(&self) -> bool { + self.alternate + } + + /// Get zero padding mode. + pub fn get_zero_pad(&self) -> bool { + self.zero_pad + } + + /// Get debug as hex mode. + pub fn get_debug_as_hex(&self) -> Option { + self.debug_as_hex + } + + /// Get width. + pub fn get_width(&self) -> Option { + self.width + } + + /// Get precision. + pub fn get_precision(&self) -> Option { + self.precision + } +} + +impl Default for FormatSpec { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::{Alignment, DebugAsHex, DisplayHint, FormatSpec, Sign}; + + #[test] + fn test_new() { + let format_spec = FormatSpec::new(); + + assert!(format_spec.get_display_hint() == DisplayHint::NoHint); + assert_eq!(format_spec.get_fill(), ' '); + assert!(format_spec.get_align().is_none()); + assert!(format_spec.get_sign().is_none()); + assert!(!format_spec.get_alternate()); + assert!(!format_spec.get_zero_pad()); + assert!(format_spec.get_debug_as_hex().is_none()); + assert!(format_spec.get_width().is_none()); + assert!(format_spec.get_precision().is_none()); + } + + #[test] + fn test_default() { + let spec_default = FormatSpec::default(); + let spec_new = FormatSpec::new(); + + assert!(spec_default.get_display_hint() == spec_new.get_display_hint()); + assert!(spec_default.get_fill() == spec_new.get_fill()); + assert!(spec_default.get_align() == spec_new.get_align()); + assert!(spec_default.get_sign() == spec_new.get_sign()); + assert!(spec_default.get_alternate() == spec_new.get_alternate()); + assert!(spec_default.get_zero_pad() == spec_new.get_zero_pad()); + assert!(spec_default.get_debug_as_hex() == spec_new.get_debug_as_hex()); + assert!(spec_default.get_width() == spec_new.get_width()); + assert!(spec_default.get_precision() == spec_new.get_precision()); + } + + #[test] + fn test_from_params() { + let display_hint = DisplayHint::Binary; + let fill = 'Z'; + let align = Some(Alignment::Right); + let sign = Some(Sign::Plus); + let alternate = true; + let zero_pad = true; + let debug_as_hex = Some(DebugAsHex::Upper); + let width = Some(1234); + let precision = Some(5); + + let format_spec = FormatSpec::from_params(display_hint, fill, align, sign, alternate, zero_pad, debug_as_hex, width, precision); + + assert!(format_spec.get_display_hint() == display_hint); + assert!(format_spec.get_fill() == fill); + assert!(format_spec.get_align() == align); + assert!(format_spec.get_sign() == sign); + assert!(format_spec.get_alternate() == alternate); + assert!(format_spec.get_zero_pad() == zero_pad); + assert!(format_spec.get_debug_as_hex() == debug_as_hex); + assert!(format_spec.get_width() == width); + assert!(format_spec.get_precision() == precision); + } + + #[test] + fn test_display_hint() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_display_hint() == DisplayHint::NoHint); + format_spec.display_hint(DisplayHint::LowerExp); + assert!(format_spec.get_display_hint() == DisplayHint::LowerExp); + } + + #[test] + fn test_fill() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_fill() == ' '); + format_spec.fill('c'); + assert!(format_spec.get_fill() == 'c'); + } + + #[test] + fn test_align() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_align().is_none()); + format_spec.align(Some(Alignment::Center)); + assert!(format_spec.get_align() == Some(Alignment::Center)); + } + + #[test] + fn test_sign() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_sign().is_none()); + format_spec.sign(Some(Sign::Minus)); + assert!(format_spec.get_sign() == Some(Sign::Minus)); + } + + #[test] + fn test_alternate() { + let mut format_spec = FormatSpec::new(); + assert!(!format_spec.get_alternate()); + format_spec.alternate(true); + assert!(format_spec.get_alternate()); + } + + #[test] + fn test_zero_pad() { + let mut format_spec = FormatSpec::new(); + assert!(!format_spec.get_zero_pad()); + format_spec.zero_pad(true); + assert!(format_spec.get_zero_pad()); + } + + #[test] + fn test_debug_as_hex() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_debug_as_hex().is_none()); + format_spec.debug_as_hex(Some(DebugAsHex::Lower)); + assert!(format_spec.get_debug_as_hex() == Some(DebugAsHex::Lower)); + } + + #[test] + fn test_width() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_width().is_none()); + format_spec.width(Some(12345)); + assert!(format_spec.get_width() == Some(12345)); + } + + #[test] + fn test_precision() { + let mut format_spec = FormatSpec::new(); + assert!(format_spec.get_precision().is_none()); + format_spec.precision(Some(54321)); + assert!(format_spec.get_precision() == Some(54321)); + } +} diff --git a/src/log/mw_log_fmt/lib.rs b/src/log/mw_log_fmt/lib.rs new file mode 100644 index 00000000..be6db5b6 --- /dev/null +++ b/src/log/mw_log_fmt/lib.rs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Implementation of formatting library. +//! Allows creation of message frames that are not exclusively text based. +//! +//! Replacement for [`core::fmt`]. + +mod builders; +mod fmt; +mod fmt_impl; +#[cfg(feature = "qm")] +mod fmt_impl_qm; +mod fmt_spec; +mod macros; + +pub use builders::{DebugList, DebugMap, DebugSet, DebugStruct, DebugTuple}; +pub use fmt::*; +pub use fmt_spec::*; + +#[cfg(test)] +mod test_utils; diff --git a/src/log/mw_log_fmt/macros.rs b/src/log/mw_log_fmt/macros.rs new file mode 100644 index 00000000..0f324e62 --- /dev/null +++ b/src/log/mw_log_fmt/macros.rs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Writes data using provided writer. +/// +/// This macro accepts a writer, a format string, and a list of arguments. +/// Arguments will be formatted according to the specified format string and the result will be passed to the writer. +#[macro_export] +macro_rules! score_write { + ($dst:expr, $($arg:tt)*) => { + // TODO: `mw_log::__private_api` will become available in future PRs. + $crate::write($dst, mw_log::__private_api::format_args!($($arg)*)) + }; +} + +/// Writes data using provided writer, with a newline appended. +/// +/// For more information, see [`score_write!`]. +#[macro_export] +macro_rules! score_writeln { + ($dst:expr $(,)?) => { + $crate::score_write!($dst, "\n") + }; + ($dst:expr, $($arg:tt)*) => { + // TODO: `mw_log::__private_api` will become available in future PRs. + $crate::write($dst, mw_log::__private_api::format_args_nl!($($arg)*)) + }; +} diff --git a/src/log/mw_log_fmt/test_utils.rs b/src/log/mw_log_fmt/test_utils.rs new file mode 100644 index 00000000..729777a0 --- /dev/null +++ b/src/log/mw_log_fmt/test_utils.rs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Common testing utilities. + +use crate::{Error, FormatSpec, Result, ScoreDebug, ScoreWrite}; +use core::fmt::{Error as CoreFmtError, Write}; + +impl From for Error { + fn from(_value: CoreFmtError) -> Self { + Error + } +} + +pub(crate) struct StringWriter { + buf: String, +} + +impl StringWriter { + pub fn new() -> Self { + Self { buf: String::new() } + } + + pub fn get(&self) -> &str { + self.buf.as_str() + } +} + +impl ScoreWrite for StringWriter { + fn write_bool(&mut self, v: &bool, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_f32(&mut self, v: &f32, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_f64(&mut self, v: &f64, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_i8(&mut self, v: &i8, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_i16(&mut self, v: &i16, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_i32(&mut self, v: &i32, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_i64(&mut self, v: &i64, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_u8(&mut self, v: &u8, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_u16(&mut self, v: &u16, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_u32(&mut self, v: &u32, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_u64(&mut self, v: &u64, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } + + fn write_str(&mut self, v: &str, _spec: &FormatSpec) -> Result { + Ok(write!(self.buf, "{}", v)?) + } +} + +/// Common test comparing [`ScoreDebug`] with [`core::fmt::Debug`]. +/// This is useful for e.g., checking string primitives. +pub(crate) fn common_test_debug(v: T) { + let mut w = StringWriter::new(); + let _ = ScoreDebug::fmt(&v, &mut w, &FormatSpec::new()); + assert_eq!(w.get(), format!("{v:?}")); +} diff --git a/src/log/src/lib.rs b/src/log/src/lib.rs deleted file mode 100644 index 7d785351..00000000 --- a/src/log/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -// NOTE: this library is a placeholder until actual library is merged. -// Cargo workspace requires any member to be present, otherwise CI/CD won't be able to pass. - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -}