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..ea68cdd
--- /dev/null
+++ b/src/print/macros.rs
@@ -0,0 +1,73 @@
+#[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),
+ }
+ }
+}