timber_rust/service/write/
boxedfmt.rs

1use crate::{Fallback, LoggerStatus, Message, Service};
2use std::any::Any;
3use std::sync::Mutex;
4use crate::service::ServiceError;
5use crate::service::write::{StandardMessageFormatter, MessageFormatter};
6
7/// A private synchronization container for heap-allocated string writers.
8///
9/// Unlike [`crate::service::write::fmt::FmtWriteServiceData`], this struct explicitly holds a trait object
10/// ([`Box<dyn std::fmt::Write>`]). This separation allows us to handle the unique
11/// borrowing requirements of boxed trait objects within the [`Service`] implementation.
12struct BoxedFmtServiceData<F>
13where
14    F: MessageFormatter,
15{
16    /// A heap-allocated, dynamically dispatched writer implementing [`std::fmt::Write`].
17    /// Must be [`Send`] + [`Sync`] to allow the [`Service`] to move between threads.
18    writer: Box<dyn std::fmt::Write + Send + Sync>,
19    /// The strategy used to format the [`Message`].
20    formatter: F,
21}
22
23/// A specialized [`Service`] for dynamically dispatched string-based logging.
24///
25/// ### Why this exists (The "Orphan Rule" Workaround)
26/// In Rust, [`std::fmt::Write`] is not implemented for `Box<dyn std::fmt::Write>`.
27/// While we could use a type alias for byte-based writers ([`BoxedFmt`] service),
28/// doing so for string-based writers would require implementing a foreign trait on
29/// a foreign type, which is forbidden by Rust's "Orphan Rules."
30///
31/// [`BoxedFmt`] service solves this by providing a concrete struct that "wraps"
32/// the boxed trait object, allowing us to manually dispatch the write calls in the
33/// [`work`](Self::work) method.
34///
35///
36pub struct BoxedFmt<F>
37where
38    F: MessageFormatter,
39{
40    /// Mutex-protected storage for the boxed writer and formatter.
41    writer: Mutex<BoxedFmtServiceData<F>>,
42}
43
44impl<F> BoxedFmt<F>
45where
46    F: MessageFormatter,
47{
48    /// Creates a new [`BoxedFmt`] service on the heap.
49    ///
50    /// # Parameters
51    /// - `writer`: A boxed trait object. This is useful when the exact type of
52    ///   the string writer (e.g., a custom UI buffer vs. a standard [`String`])
53    ///   is not known at compile time.
54    /// - `formatter`: The [`MessageFormatter`] implementation.
55    pub fn new(writer: Box<dyn std::fmt::Write + Send + Sync>) -> Box<Self> {
56        Box::new(Self {
57            writer: Mutex::new(BoxedFmtServiceData {
58                writer,
59                formatter: Default::default(),
60            }),
61        })
62    }
63
64    /// Creates a new [`BoxedFmt`] service on the heap with a custom [formatter][`MessageFormatter`].
65    ///
66    /// # Parameters
67    /// - `writer`: A boxed trait object. This is useful when the exact type of
68    ///   the string writer (e.g., a custom UI buffer vs. a standard [`String`])
69    ///   is not known at compile time.
70    /// - `formatter`: The [`MessageFormatter`] implementation.
71    pub fn with_formatter(
72        writer: Box<dyn std::fmt::Write + Send + Sync>,
73        formatter: F,
74    ) -> Box<Self> {
75        Box::new(Self {
76            writer: Mutex::new(BoxedFmtServiceData { writer, formatter }),
77        })
78    }
79
80    /// Allows safe, read-only access to the internal buffer without stopping the logger.
81    ///
82    /// Use this to "peek" at logs while the application is still running—perfect for
83    /// health-check endpoints that expose recent logs or for verifying state in tests.
84    ///
85    /// ### Thread Safety
86    /// This method acquires a mutex lock. While the closure `f` is executing, any
87    /// incoming logs from other threads will **block** until the closure returns.
88    /// Keep the logic inside the closure as fast as possible.
89    ///
90    /// ### Returns
91    /// - [`Some(R)`][`Some`]: The result of your closure if the lock was acquired.
92    /// - [`None`]: If the internal lock was poisoned by a previous panic.
93    pub fn inspect_writer<R>(
94        &self,
95        f: impl FnOnce(&Box<dyn std::fmt::Write + Send + Sync>) -> R,
96    ) -> Option<R> {
97        self.writer.lock().ok().map(|data| f(&data.writer))
98    }
99
100    /// Destroys the [`Service`] and reclaims ownership of the underlying buffer or writer.
101    ///
102    /// Use this at the end of a program, a test case, or a lifecycle stage to extract
103    /// all recorded logs and free up the resources used by the [`Service`].
104    ///
105    /// ### Ownership & Lifecycle
106    /// This method consumes `self`, meaning the [`BoxedFmt`] service can no longer be
107    /// used after this call. This is the only way to get full, non-cloned ownership
108    /// of the internal writer (e.g., a [`String`] or [`Vec<u8>`]).
109    ///
110    /// ### Guarantees
111    /// Because this takes ownership of the [`Service`], it is compile-time guaranteed
112    /// that no other threads can be writing to the buffer when this is called.
113    pub fn take_writer(self) -> Result<Box<dyn std::fmt::Write + Send + Sync>, ServiceError> {
114        let data = self.writer.into_inner();
115        match data {
116            Ok(data) => Ok(data.writer),
117            Err(_) => Err(ServiceError::LockPoisoned),
118        }
119    }
120}
121
122impl<F> Service for BoxedFmt<F>
123where
124    F: MessageFormatter + 'static,
125{
126    /// Returns the current operational status.
127    fn status(&self) -> LoggerStatus {
128        LoggerStatus::Running
129    }
130
131    /// Acquires the lock and dispatches the write to the boxed trait object.
132    ///
133    /// # Internal Mechanics
134    /// Since `Box<dyn std::fmt::Write>` doesn't implement [`std::fmt::Write`], this method uses
135    /// [`Box::as_mut()`] to obtain a mutable reference to the underlying
136    /// trait object before passing it to the formatter.
137    ///
138    /// # Errors
139    /// - [`ServiceError::LockPoisoned`] if the mutex is poisoned
140    /// - Forwards any [`ServiceError`] returned by the formatter.
141    fn work(&self, msg: &Message) -> Result<(), ServiceError> {
142        let mut guard = self.writer.lock()?;
143
144        // Destructure the internal data
145        let BoxedFmtServiceData {
146            formatter, writer, ..
147        } = &mut *guard;
148
149        // Manual dispatch: conversion from Box<dyn Write> to &mut dyn Write
150        formatter.format_fmt(msg, writer.as_mut())?;
151        Ok(())
152    }
153
154    /// Returns a reference to the underlying type as [Any] for downcasting.
155    fn as_any(&self) -> &dyn Any {
156        self
157    }
158}
159
160impl<F> Fallback for BoxedFmt<F>
161where
162    F: MessageFormatter + 'static,
163{
164    /// Emergency fallback that redirects output to `stdout`.
165    ///
166    /// If the primary boxed writer is inaccessible or failing, the message
167    /// is formatted using the standard I/O fallback path.
168    fn fallback(&self, error: &ServiceError, msg: &Message) {
169        if let Ok(mut guard) = self.writer.lock() {
170            let mut out = std::io::stdout();
171            let _ = guard.formatter.format_io(msg, &mut out);
172            let _ = eprintln!("BoxedFmtWriteService Fallback [Error: {}]", error);
173        }
174    }
175}
176
177/// A [`BoxedFmt`] service pre-configured with the [`StandardMessageFormatter`].
178///
179/// This type is commonly used as a catch-all for string-based logging where
180/// maximum flexibility is required for the output destination.
181pub type StandardBoxedFmt = BoxedFmt<StandardMessageFormatter>;