diff --git a/examples/printer.rs b/examples/printer.rs new file mode 100644 index 0000000..431ebee --- /dev/null +++ b/examples/printer.rs @@ -0,0 +1,34 @@ +use kxio::{kxprintln, print::Printer}; + +// Example using kxprintln macro +fn greet(printer: &Printer, name: &str) { + kxprintln!(printer, "Hello, {}!", name); +} + +fn main() { + // Production code example + let printer = Printer::standard(); + kxprintln!(printer, "Macro says: Hello, {}!", "Carol"); + greet(&printer, "Carol"); +} + +#[cfg(test)] +mod test { + use super::*; + + use kxio::print::TestPrint; + + #[test] + fn test_printer() { + // Test code + let printer = Printer::test(); + // Get reference to TestPrinter so we can make assertions + let test_print = printer.as_test().unwrap(); + greet(&printer, "Bob"); + assert_eq!(test_print.output(), "Hello, Bob!\n"); + test_print.clear(); + + greet(&printer, "Dave"); + assert_eq!(test_print.output(), "Hello, Dave!\n"); + } +} diff --git a/src/lib.rs b/src/lib.rs index a75dcc0..086b294 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,15 @@ //! # kxio //! -//! `kxio` is a Rust library that provides injectable `FileSystem` and `Network` +//! `kxio` is a Rust library that provides injectable `FileSystem`, `Network` and `Printer` //! resources to enhance the testability of your code. By abstracting system-level //! interactions, `kxio` enables easier mocking and testing of code that relies on -//! file system and network operations. +//! file system, network and print operations. //! //! ## Features //! //! - Filesystem Abstraction //! - Network Abstraction +//! - Print Abstraction //! - Enhanced Testability //! //! ## Filesystem @@ -30,6 +31,11 @@ //! comprehensive documentation and usage examples, please refer to //! //! +//! ## Printer +//! +//! No, not a hardware printer, but console output via the family of `println` macros from the +//! Standard Library. +//! //! ## Getting Started //! //! Add `kxio` to your `Cargo.toml`: @@ -42,7 +48,7 @@ //! ## Usage //! //! See the example [get.rs](https://git.kemitix.net/kemitix/kxio/src/branch/main/examples/get.rs) for an annotated example on how to use the `kxio` library. -//! It covers both the `net` and `fs` modules. +//! It covers the `net`, `fs` and `print` modules. //! //! ## Development //! @@ -65,6 +71,7 @@ pub mod fs; pub mod net; +pub mod print; mod result; pub use result::{Error, Result}; diff --git a/src/print/macros.rs b/src/print/macros.rs new file mode 100644 index 0000000..0ac8202 --- /dev/null +++ b/src/print/macros.rs @@ -0,0 +1,74 @@ +#[macro_export] +macro_rules! kxprintln { + ($printer:expr, $($arg:tt)*) => {{ + $crate::print::Print::println_fmt(&$printer.clone(), format_args!($($arg)*)) + }}; +} + +#[macro_export] +macro_rules! kxprint { + ($printer:expr, $($arg:tt)*) => {{ + $crate::print::Print::print_fmt(&$printer.clone(), format_args!($($arg)*)) + }}; +} + +#[macro_export] +macro_rules! kxeprintln { + ($printer:expr, $($arg:tt)*) => {{ + $crate::print::Print::eprintln_fmt(&$printer.clone(), format_args!($($arg)*)) + }}; +} + +#[macro_export] +macro_rules! kxeprint { + ($printer:expr, $($arg:tt)*) => {{ + $crate::print::Print::eprint_fmt(&$printer.clone(), format_args!($($arg)*)) + }}; +} + + +#[cfg(test)] +mod tests { + use super::super::TestPrint; + + #[test] + fn test_macro_with_ref_and_owned() { + // Test with reference + let printer = TestPrint::new(); + kxprintln!(&printer, "ref test"); + assert_eq!(printer.output(), "ref test\n"); + printer.clear(); + + // Test with owned value + kxprintln!(printer, "owned test"); + assert_eq!(printer.output(), "owned test\n"); + } + + #[test] + fn test_kxprint_macro() { + let printer = TestPrint::new(); + kxprint!(printer, "Hello {}", "world"); + assert_eq!(printer.output(), "Hello world"); + } + + #[test] + fn test_kxprintln_macro() { + let printer = TestPrint::new(); + kxprintln!(printer, "Hello {}", "world"); + assert_eq!(printer.output(), "Hello world\n"); + } + + #[test] + fn test_kxeprint_macro() { + let printer = TestPrint::new(); + kxeprint!(printer, "Error: {}", "file not found"); + assert_eq!(printer.stderr(), "Error: file not found"); + } + + #[test] + fn test_kxeprintln_macro() { + let printer = TestPrint::new(); + kxeprintln!(printer, "Error: {}", "permission denied"); + assert_eq!(printer.stderr(), "Error: permission denied\n"); + } +} diff --git a/src/print/mod.rs b/src/print/mod.rs new file mode 100644 index 0000000..a7399be --- /dev/null +++ b/src/print/mod.rs @@ -0,0 +1,343 @@ +//! Provides an injectable interface for standard print operations. +//! +//! This module also includes macros for println and print operations that +//! are similar to the standard library but work with the Print trait. +//! +//! This module offers a trait-based abstraction over printing operations, +//! allowing for dependency injection and easier testing of code that performs +//! printing operations. +//! +//! # Examples +//! +//! ``` +//! use kxio::print::{Print, StandardPrint}; +//! +//! fn print_hello(printer: &impl Print) { +//! printer.println("Hello, World!"); +//! } +//! +//! let printer = StandardPrint; +//! print_hello(&printer); +//! ``` + +mod macros; +mod printer; + +pub use printer::Printer; +use std::fmt::Arguments; + +/// Standard implementation of the Print trait that uses the std::print! and std::println! macros. +pub fn standard() -> Printer { + Printer::standard() +} + +/// A no-op implementation of the Print trait that discards all output. +pub fn null() -> Printer { + Printer::null() +} + +/// A test implementation that captures output in a String. +pub fn test() -> Printer { + Printer::test() +} + +/// Trait defining print operations that can be performed by implementors. +pub trait Print: Clone { + /// Prints a formatted line to the standard output. + /// + /// # Arguments + /// + /// * `args` - Format arguments to print + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.print_fmt(format_args!("Hello")); + /// ``` + fn print_fmt(&self, args: Arguments<'_>); + + /// Prints a formatted line followed by a newline to the standard output. + /// + /// # Arguments + /// + /// * `args` - Format arguments to print + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.println_fmt(format_args!("Hello")); + /// ``` + fn println_fmt(&self, args: Arguments<'_>); + + /// Prints a string slice to the standard output. + /// + /// # Arguments + /// + /// * `s` - The string slice to print + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.print("Hello"); + /// ``` + fn print(&self, s: &str) { + self.print_fmt(format_args!("{}", s)); + } + + /// Prints a string slice followed by a newline to the standard output. + /// + /// # Arguments + /// + /// * `s` - The string slice to print + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.println("Hello"); + /// ``` + fn println(&self, s: &str) { + self.println_fmt(format_args!("{}", s)); + } + + /// Prints a string slice to the standard error. + /// + /// # Arguments + /// + /// * `s` - The string slice to print to stderr + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.eprint("Error"); + /// ``` + fn eprint(&self, s: &str) { + self.eprint_fmt(format_args!("{}", s)); + } + + /// Prints a string slice followed by a newline to the standard error. + /// + /// # Arguments + /// + /// * `s` - The string slice to print to stderr + /// + /// # Examples + /// + /// ``` + /// use kxio::print::{Print, StandardPrint}; + /// + /// let printer = StandardPrint; + /// printer.eprintln("Error"); + /// ``` + fn eprintln(&self, s: &str) { + self.eprintln_fmt(format_args!("{}", s)); + } + + /// Prints to stderr with a format string. + /// + /// This method is the base method for printing to stderr. The other stderr printing + /// methods are implemented in terms of this one. + /// + /// # Arguments + /// + /// * `args` - The format arguments to print + fn eprint_fmt(&self, args: Arguments<'_>); + + /// Prints to stderr with a format string, followed by a newline. + /// + /// This method is the base method for printing to stderr with a newline. The other stderr printing + /// methods are implemented in terms of this one. + /// + /// # Arguments + /// + /// * `args` - The format arguments to print + fn eprintln_fmt(&self, args: Arguments<'_>); +} + +/// Standard implementation of the Print trait that uses the std::print! and std::println! macros. +#[derive(Clone, Debug, Default, Copy)] +pub struct StandardPrint; + +impl Print for StandardPrint { + fn print_fmt(&self, args: Arguments<'_>) { + std::print!("{}", args); + } + + fn println_fmt(&self, args: Arguments<'_>) { + std::println!("{}", args); + } + + fn eprint_fmt(&self, args: Arguments<'_>) { + std::eprint!("{}", args); + } + + fn eprintln_fmt(&self, args: Arguments<'_>) { + std::eprintln!("{}", args); + } +} + +/// A no-op implementation of the Print trait that discards all output. +#[derive(Clone, Debug, Default, Copy)] +pub struct NullPrint; + +impl Print for NullPrint { + fn print_fmt(&self, _args: Arguments<'_>) {} + fn println_fmt(&self, _args: Arguments<'_>) {} + fn eprint_fmt(&self, _args: Arguments<'_>) {} + fn eprintln_fmt(&self, _args: Arguments<'_>) {} +} + +/// A test implementation that captures output in a String. +#[derive(Clone, Debug, Default)] +pub struct TestPrint { + stdout: std::sync::Arc>, + stderr: std::sync::Arc>, +} + +impl TestPrint { + /// Creates a new TestPrint instance. + pub fn new() -> Self { + Self { + stdout: std::sync::Arc::new(std::sync::Mutex::new(String::new())), + stderr: std::sync::Arc::new(std::sync::Mutex::new(String::new())), + } + } + + /// Returns the captured stdout output as a String. + pub fn output(&self) -> String { + self.stdout.lock().unwrap().clone() + } + + /// Returns the captured stderr output as a String. + pub fn stderr(&self) -> String { + self.stderr.lock().unwrap().clone() + } + + /// Clears both the captured stdout and stderr output. + pub fn clear(&self) { + self.stdout.lock().unwrap().clear(); + self.stderr.lock().unwrap().clear(); + } +} + +impl Print for TestPrint { + fn print_fmt(&self, args: Arguments<'_>) { + (&self).print_fmt(args) + } + + fn println_fmt(&self, args: Arguments<'_>) { + (&self).println_fmt(args) + } + + fn eprint_fmt(&self, args: Arguments<'_>) { + (&self).eprint_fmt(args) + } + + fn eprintln_fmt(&self, args: Arguments<'_>) { + (&self).eprintln_fmt(args) + } +} +impl Print for &TestPrint { + fn print_fmt(&self, args: Arguments<'_>) { + let mut output = self.stdout.lock().unwrap(); + output.push_str(&format!("{}", args)); + } + + fn println_fmt(&self, args: Arguments<'_>) { + let mut output = self.stdout.lock().unwrap(); + output.push_str(&format!("{}\n", args)); + } + + fn eprint_fmt(&self, args: Arguments<'_>) { + let mut output = self.stderr.lock().unwrap(); + output.push_str(&format!("{}", args)); + } + + fn eprintln_fmt(&self, args: Arguments<'_>) { + let mut output = self.stderr.lock().unwrap(); + output.push_str(&format!("{}\n", args)); + } +} + +#[cfg(test)] +mod tests { + use crate::{kxprint, kxprintln}; + + use super::*; + + #[test] + fn test_standard_print() { + let printer = Printer::standard(); + // Note: This will actually print to stdout + printer.println("This is a test"); + kxprintln!(printer, "This is a {} test", "macro"); + } + + #[test] + fn test_null_print() { + let printer = Printer::null(); + printer.println("This should not appear anywhere"); + kxprintln!(printer, "This should also, {}", "not appear anywhere"); + } + + #[test] + fn test_test_print() { + let printer = Printer::test(); + // we are interleaving printing, with assertions, so just extraxt the TestPrinter reference now + let test_print = printer.as_test().unwrap(); + + // Test stdout functions + printer.print("Hello"); + kxprint!(printer, " "); + printer.println("World"); + assert_eq!(test_print.output(), "Hello World\n"); + + // Test stderr functions + printer.eprint("Error: "); + printer.eprintln("something went wrong"); + assert_eq!(test_print.stderr(), "Error: something went wrong\n"); + + // Test clear function clears both buffers + test_print.clear(); + assert_eq!(test_print.output(), ""); + assert_eq!(test_print.stderr(), ""); + + // Verify separate stdout/stderr streams + printer.println("info: running task"); + printer.eprintln("error: task failed"); + assert_eq!(test_print.output(), "info: running task\n"); + assert_eq!(test_print.stderr(), "error: task failed\n"); + + test_print.clear(); + printer.println("New message"); + kxprintln!(printer, "second line"); + assert_eq!(test_print.output(), "New message\nsecond line\n"); + } + + #[test] + fn test_print_in_function() { + fn print_message(printer: &Printer) { + printer.println("Test message"); + kxprintln!(printer, "{} test message", "Second"); + } + + let printer = Printer::test(); + let test_print = printer.as_test().unwrap(); + print_message(&printer); + assert_eq!(test_print.output(), "Test message\nSecond test message\n"); + } +} diff --git a/src/print/printer.rs b/src/print/printer.rs new file mode 100644 index 0000000..a7f86bc --- /dev/null +++ b/src/print/printer.rs @@ -0,0 +1,68 @@ +use super::{NullPrint, Print, StandardPrint, TestPrint}; + +/// A wrapper struct that can contain any implementation of the Print trait +#[derive(Clone)] +pub enum Printer { + Standard(StandardPrint), + Null(NullPrint), + Test(TestPrint), +} + +impl Printer { + /// Creates a new Printer wrapping a StandardPrint implementation + pub fn standard() -> Self { + Self::Standard(StandardPrint) + } + + /// Creates a new Printer wrapping a NullPrint implementation + pub fn null() -> Self { + Self::Null(NullPrint) + } + + /// Creates a new Printer wrapping a TestPrint implementation + pub fn test() -> Self { + Self::Test(TestPrint::new()) + } + + /// Returns a reference to the wrapped TestPrint implementation if this Printer contains one + pub fn as_test(&self) -> Option<&TestPrint> { + match self { + Self::Test(test_print) => Some(test_print), + _ => None, + } + } +} + +impl Print for Printer { + fn print_fmt(&self, args: std::fmt::Arguments<'_>) { + match self { + Self::Standard(p) => p.print_fmt(args), + Self::Null(p) => p.print_fmt(args), + Self::Test(p) => p.print_fmt(args), + } + } + + fn println_fmt(&self, args: std::fmt::Arguments<'_>) { + match self { + Self::Standard(p) => p.println_fmt(args), + Self::Null(p) => p.println_fmt(args), + Self::Test(p) => p.println_fmt(args), + } + } + + fn eprint_fmt(&self, args: std::fmt::Arguments<'_>) { + match self { + Self::Standard(p) => p.eprint_fmt(args), + Self::Null(p) => p.eprint_fmt(args), + Self::Test(p) => p.eprint_fmt(args), + } + } + + fn eprintln_fmt(&self, args: std::fmt::Arguments<'_>) { + match self { + Self::Standard(p) => p.eprintln_fmt(args), + Self::Null(p) => p.eprintln_fmt(args), + Self::Test(p) => p.eprintln_fmt(args), + } + } +}