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>;