aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/any.rs122
-rw-r--r--mingling_core/src/any/group.rs12
-rw-r--r--mingling_core/src/asset/dispatcher.rs6
-rw-r--r--mingling_core/src/asset/global_resource.rs6
-rw-r--r--mingling_core/src/lib.rs8
-rw-r--r--mingling_core/src/program/collection/mock.rs7
-rw-r--r--mingling_core/src/program/hook.rs15
-rw-r--r--mingling_core/src/program/once_exec.rs38
-rw-r--r--mingling_core/src/program/setup.rs3
-rw-r--r--mingling_core/src/renderer/render_result.rs497
-rw-r--r--mingling_core/src/renderer/structural.rs16
-rw-r--r--mingling_core/src/renderer/structural/structural_data.rs8
12 files changed, 612 insertions, 126 deletions
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs
index ec29a1b..3e8fdf0 100644
--- a/mingling_core/src/any.rs
+++ b/mingling_core/src/any.rs
@@ -22,14 +22,14 @@ pub struct AnyOutput<G> {
///
/// This is set during construction and used for type-checking
/// in downcast, restore, and is methods.
- pub type_id: std::any::TypeId,
+ pub(crate) type_id: std::any::TypeId,
/// The variant identifier returned by [`Grouped::member_id`] for the
/// concrete type stored in `inner`.
///
/// This is used by the scheduler to dispatch on the correct enum
/// variant when routing the output.
- pub member_id: G,
+ pub(crate) member_id: G,
}
impl<G> AnyOutput<G> {
@@ -45,6 +45,52 @@ impl<G> AnyOutput<G> {
}
}
+ /// Create an `AnyOutput` from a raw value with a manually specified member_id.
+ ///
+ /// This function bypasses the [`Grouped`] trait, meaning the `member_id` you provide
+ /// does **not** have to match the actual concrete type `T`. The scheduler uses
+ /// `member_id` to determine which enum variant the output belongs to, and later
+ /// attempts to restore the value to the concrete type `T` based on that variant.
+ ///
+ /// # Safety
+ ///
+ /// - The caller must ensure that `member_id` correctly corresponds to the concrete
+ /// type `T` according to the scheduling logic. If `member_id` does not match,
+ /// calling [`restore`](Self::restore) or [`downcast`](Self::downcast) with the
+ /// type associated with `member_id` will cause **undefined behavior**.
+ /// - This safety contract is the caller's responsibility; the compiler cannot
+ /// enforce the correspondence between `member_id` and the stored type.
+ pub unsafe fn new_bare<T>(value: T, member_id: G) -> Self
+ where
+ T: Send + 'static,
+ {
+ Self {
+ inner: Box::new(value),
+ type_id: std::any::TypeId::of::<T>(),
+ member_id,
+ }
+ }
+
+ /// Get the [`TypeId`] of the concrete type stored in `inner`.
+ ///
+ /// The `TypeId` is set during construction (via [`AnyOutput::new`] or [`AnyOutput::new_bare`])
+ /// and is used for subsequent downcasting and type checking.
+ pub fn type_id(&self) -> std::any::TypeId {
+ self.type_id
+ }
+
+ /// Get the [`member_id`] of the concrete type stored in `inner`.
+ ///
+ /// [`member_id`] is set during construction (via [`AnyOutput::new`] or [`AnyOutput::new_bare`])
+ /// and identifies which variant of the output enum this value corresponds to.
+ /// The scheduler uses this value to dispatch the output to the correct next step.
+ pub fn member_id(&self) -> G
+ where
+ G: Copy,
+ {
+ self.member_id
+ }
+
/// Attempt to downcast the `AnyOutput` to a concrete type.
///
/// # Errors
@@ -190,7 +236,17 @@ mod tests {
value: i32,
}
- impl Grouped<MockGroup> for AlphaData {
+ /// # Safety
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for AlphaData {
fn member_id() -> MockGroup {
MockGroup::Alpha
}
@@ -202,7 +258,17 @@ mod tests {
name: String,
}
- impl Grouped<MockGroup> for BetaData {
+ /// # Safety
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for BetaData {
fn member_id() -> MockGroup {
MockGroup::Beta
}
@@ -213,7 +279,17 @@ mod tests {
#[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))]
struct GammaData;
- impl Grouped<MockGroup> for GammaData {
+ /// # Safety
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for GammaData {
fn member_id() -> MockGroup {
MockGroup::Gamma
}
@@ -369,7 +445,17 @@ mod tests {
x: i32,
}
- impl Grouped<MockGroup> for SerData {
+ /// SAFETY:
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for SerData {
fn member_id() -> MockGroup {
MockGroup::Gamma
}
@@ -396,13 +482,33 @@ mod tests {
b: String,
}
- impl Grouped<MockGroup> for SerA {
+ /// SAFETY:
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for SerA {
fn member_id() -> MockGroup {
MockGroup::Alpha
}
}
- impl Grouped<MockGroup> for SerB {
+ /// SAFETY:
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockGroup> for SerB {
fn member_id() -> MockGroup {
MockGroup::Beta
}
diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs
index 2813ad5..5e5e347 100644
--- a/mingling_core/src/any/group.rs
+++ b/mingling_core/src/any/group.rs
@@ -2,10 +2,14 @@ use crate::{AnyOutput, ChainProcess, ProgramCollect, Routable};
/// Used to mark a type with a unique enum ID, assisting dynamic dispatch
///
-/// **Note:** Unlike earlier versions, `Grouped` no longer requires `Serialize`
-/// even when the `structural_renderer` feature is enabled. Structured output is
-/// controlled separately via the \[`StructuralData`\] trait.
-pub trait Grouped<Group>
+/// # Safety
+///
+/// The returned `Group` value is an enum variant created by `register_type!` when
+/// registering the type's ID. Whether the variant matches correctly is guaranteed
+/// by `Grouped derive` or macros like `pack!`. If implemented manually, and the
+/// type name written in `member_id()` does not match the actually registered type,
+/// dispatching to that type will result in **100% undefined behavior**.
+pub unsafe trait Grouped<Group>
where
Self: Sized + 'static,
{
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
index 01c9ccf..cb0987d 100644
--- a/mingling_core/src/asset/dispatcher.rs
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -38,7 +38,7 @@ where
note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
)
)]
- pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp)
+ pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp) -> &mut Self
where
Disp: Dispatcher<C> + Send + Sync + 'static,
{
@@ -50,6 +50,7 @@ where
{
let _ = dispatcher;
}
+ self
}
/// Add some dispatchers to the program.
@@ -59,7 +60,7 @@ where
note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
)
)]
- pub fn with_dispatchers<D>(&mut self, dispatchers: D)
+ pub fn with_dispatchers<D>(&mut self, dispatchers: D) -> &mut Self
where
D: Into<Dispatchers<C>>,
{
@@ -72,6 +73,7 @@ where
{
let _ = dispatchers;
}
+ self
}
}
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index 29e1136..a610378 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -13,10 +13,14 @@ where
C: ProgramCollect<Enum = C>,
{
/// Insert a resource of the given type, cloning the provided value into the store
- pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(&mut self, res: Res) {
+ pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(
+ &mut self,
+ res: Res,
+ ) -> &mut Self {
if let Ok(mut guard) = self.resources.lock() {
guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(res)));
}
+ self
}
/// Modify a resource by type, applying a closure to the resource if present
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 2cb24b9..31476c9 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -90,8 +90,14 @@ pub mod setup {
/// Private API — not intended for direct use.
#[doc(hidden)]
pub mod __private {
+ use crate::ProgramCollect;
+
/// Sealed trait for `StructuralData` — only implementable via derive macro.
- pub trait StructuralDataSealed {}
+ pub trait StructuralDataSealed<C>
+ where
+ C: ProgramCollect<Enum = C>,
+ {
+ }
/// Re-export so the derive macro can reference the trait without
/// conflicting with the derive macro name at `::mingling::StructuralData`.
diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs
index 5847f10..dbe4789 100644
--- a/mingling_core/src/program/collection/mock.rs
+++ b/mingling_core/src/program/collection/mock.rs
@@ -23,9 +23,12 @@ pub enum MockProgramCollect {
Bar,
}
-impl Grouped<MockProgramCollect> for MockProgramCollect {
+/// SAFETY: This is a mock type used only for temporary testing.
+/// It will never actually enter the macro system.
+/// The internal `panic!` ensures that `member_id` will never be executed.
+unsafe impl Grouped<MockProgramCollect> for MockProgramCollect {
fn member_id() -> MockProgramCollect {
- MockProgramCollect::Foo
+ panic!("Attempting to read an unsafe enum type");
}
}
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index db1691b..7d94a21 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -144,8 +144,9 @@ where
{
/// Adds a typed hook to the program. The hook will be called at the appropriate
/// lifecycle events.
- pub fn with_hook(&mut self, hook: ProgramHook<C>) {
+ pub fn with_hook(&mut self, hook: ProgramHook<C>) -> &mut Self {
self.hooks.push(hook);
+ self
}
pub(crate) fn run_hook_on_begin(&self, info: HookBeginInfo) {
@@ -694,7 +695,17 @@ mod tests {
}
}
- impl Grouped<MockHookEnum> for MockHookEnum {
+ /// SAFETY:
+ ///
+ /// This implementation is only for testing purposes to satisfy trait bounds.
+ /// Since this code only constructs `AnyOutput` and calls methods like
+ /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` —
+ /// none of which involve `ProgramCollect::do_chain` or
+ /// `ProgramCollect::render` — the type/member_id correspondence is
+ /// never exploited in an unsafe way here.
+ /// The caller must ensure that the associated `member_id` correctly
+ /// corresponds to the type's role in the group.
+ unsafe impl Grouped<MockHookEnum> for MockHookEnum {
fn member_id() -> MockHookEnum {
MockHookEnum::A
}
diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs
index 9d6f1e4..a846d04 100644
--- a/mingling_core/src/program/once_exec.rs
+++ b/mingling_core/src/program/once_exec.rs
@@ -79,23 +79,12 @@ where
};
// Read exit code
- let exit_code = result.exit_code;
-
// Render result
- if stdout_setting.render_output && !result.is_empty() {
- print!("{result}");
-
- if let Err(e) = std::io::Write::flush(&mut std::io::stdout())
- && stdout_setting.error_output
- {
- eprintln!("{e}");
- 1
- } else {
- exit_code
- }
- } else {
- exit_code
+ if stdout_setting.render_output {
+ result.std_print();
}
+
+ result.exit_code
}
/// Run the command line program, then exit
@@ -217,23 +206,12 @@ where
};
// Read exit code
- let exit_code = result.exit_code;
-
// Render result
- if stdout_setting.render_output && !result.is_empty() {
- print!("{result}");
-
- if let Err(e) = std::io::Write::flush(&mut std::io::stdout())
- && stdout_setting.error_output
- {
- eprintln!("{e}");
- 1
- } else {
- exit_code
- }
- } else {
- exit_code
+ if stdout_setting.render_output {
+ result.std_print();
}
+
+ result.exit_code
}
/// Run the command line program, then exit
diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs
index 838c29a..a8ff114 100644
--- a/mingling_core/src/program/setup.rs
+++ b/mingling_core/src/program/setup.rs
@@ -29,8 +29,9 @@ where
C: ProgramCollect<Enum = C>,
{
/// Load and execute init logic
- pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) {
+ pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) -> &mut Self {
S::setup(setup, self);
+ self
}
}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 2c60fa4..c38522a 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,13 +1,24 @@
use std::{
fmt::{Display, Formatter},
io::Write,
- ops::Deref,
};
+use crate::RenderResultMode::{Stderr, Stdout};
+
/// Render result, containing the rendered text content.
#[derive(Default, Debug, PartialEq)]
pub struct RenderResult {
- render_text: String,
+ /// Whether the output should be written immediately.
+ ///
+ /// When set to `true`, rendered content will be flushed to stdout/stderr
+ /// in real time while also being collected in the render buffer.
+ immediate_output: bool,
+
+ /// The buffered render output, stored as a list of (text, mode) pairs.
+ ///
+ /// Each entry contains a rendered string together with a `RenderResultMode`
+ /// indicating whether it should be output to stdout or stderr.
+ render_buffer: Vec<(String, RenderResultMode)>,
/// The exit code to return from the rendering process.
///
@@ -16,12 +27,41 @@ pub struct RenderResult {
pub exit_code: i32,
}
+/// Enum representing the output mode for render results.
+///
+/// This determines whether the rendered content should be directed to standard
+/// output or standard error.
+///
+/// # Variants
+///
+/// * `Stdout` - Output will be written to standard output (stdout).
+/// * `Stderr` - Output will be written to standard error (stderr).
+#[repr(u8)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+pub enum RenderResultMode {
+ /// Standard output (stdout).
+ #[default]
+ Stdout = 0,
+
+ /// Standard error (stderr).
+ Stderr = 1,
+}
+
+impl<F> From<F> for RenderResult
+where
+ F: FnOnce() -> RenderResult,
+{
+ fn from(value: F) -> Self {
+ value()
+ }
+}
+
impl Write for RenderResult {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let s = std::str::from_utf8(buf).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "not valid UTF-8")
})?;
- self.render_text.push_str(s);
+ self.append_to_buffer(s, Stdout);
Ok(buf.len())
}
@@ -32,15 +72,7 @@ impl Write for RenderResult {
impl Display for RenderResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- writeln!(f, "{}", self.render_text.trim())
- }
-}
-
-impl Deref for RenderResult {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- &self.render_text
+ write!(f, "{}", render_result_to_string(self).trim())
}
}
@@ -69,40 +101,31 @@ impl_from_int!(i32, i16, i8, u32, u16, u8, usize);
impl From<&String> for RenderResult {
fn from(value: &String) -> Self {
- RenderResult {
- render_text: value.clone(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<String> for RenderResult {
fn from(value: String) -> Self {
- RenderResult {
- render_text: value,
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<&str> for RenderResult {
fn from(value: &str) -> Self {
- RenderResult {
- render_text: value.to_string(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<RenderResult> for String {
fn from(result: RenderResult) -> Self {
- result.render_text
+ render_result_to_string(&result)
}
}
impl From<&RenderResult> for String {
fn from(result: &RenderResult) -> Self {
- result.render_text.clone()
+ render_result_to_string(result)
}
}
@@ -124,21 +147,139 @@ impl RenderResult {
Self::default()
}
+ /// Marks the render result for immediate output, bypassing any buffering or
+ /// deferred rendering.
+ ///
+ /// When set, the rendered content will be both collected in the result and
+ /// immediately flushed to stdout/stderr in real time, rather than being
+ /// deferred for later display.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.immediate_output();
+ /// ```
+ pub fn immediate_output(&mut self) -> &mut Self {
+ self.immediate_output = true;
+ self
+ }
+
+ /// Appends the given text and mode to the render buffer.
+ ///
+ /// Unlike `print` and `println` which only store plain text in a single string,
+ /// this method stores the text along with a `RenderResultMode` that indicates
+ /// whether the output should be directed to stdout or stderr. This allows for
+ /// more fine-grained control over output routing when the buffer is later flushed.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error message", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.render_buffer.push((text.into(), mode));
+ }
+
+ /// Appends the given text followed by a newline, along with the mode, to the render buffer.
+ ///
+ /// This is a convenience method that calls `append_to_buffer` for the text and then
+ /// appends a newline with the same mode.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_line_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_line_to_buffer("Warning", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_line_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.append_to_buffer(text, mode);
+ self.append_to_buffer("\n", mode);
+ }
+
+ /// Appends the contents of another `RenderResult` to this one.
+ ///
+ /// If this `RenderResult` has `immediate_output` enabled but the other does not,
+ /// the other's content will be immediately flushed to the appropriate output stream
+ /// (stdout/stderr) while also being appended to the render buffer.
+ ///
+ /// The `exit_code` of the other result is **not** transferred — only the buffered
+ /// content and the `immediate_output` flag of the other result are merged.
+ ///
+ /// # Arguments
+ ///
+ /// * `other` - The `RenderResult` whose contents should be appended.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut dest = RenderResult::default();
+ /// let mut src = RenderResult::default();
+ ///
+ /// src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// src.append_to_buffer(" Error", RenderResultMode::Stderr);
+ ///
+ /// dest.append_other(src);
+ /// assert_eq!(dest.to_string(), "Hello Error");
+ /// ```
+ pub fn append_other(&mut self, other: impl Into<RenderResult>) {
+ let other = other.into();
+
+ // If self has immediate output enabled, but the input does not, the input needs immediate output.
+ let immediate_output = !other.immediate_output && self.immediate_output;
+
+ for i in other.render_buffer {
+ if immediate_output {
+ match &i.1 {
+ Stdout => print!("{}", i.0),
+ Stderr => eprint!("{}", i.0),
+ }
+ }
+ self.render_buffer.push(i);
+ }
+ }
+
/// Appends the given text to the rendered content.
///
/// # Examples
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.print("Hello");
/// result.print(", world!");
- /// assert_eq!(result.deref(), "Hello, world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub fn print(&mut self, text: impl AsRef<str>) {
- self.render_text.push_str(text.as_ref());
+ pub fn print(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ print!("{}", text)
+ }
+ self.append_to_buffer(text, Stdout);
}
/// Appends the given text followed by a newline to the rendered content.
@@ -147,16 +288,58 @@ impl RenderResult {
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.println("First line");
/// result.println("Second line");
- /// assert_eq!(result.deref(), "First line\nSecond line\n");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn println(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stdout);
+ }
+
+ /// Appends the given text to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprint("Hello");
+ /// result.eprint(", world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
+ /// ```
+ pub fn eprint(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ eprint!("{}", text)
+ }
+ self.append_to_buffer(text, Stderr);
+ }
+
+ /// Appends the given text followed by a newline to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
/// ```
- pub fn println(&mut self, text: impl AsRef<str>) {
- self.render_text.push_str(text.as_ref());
- self.render_text.push('\n');
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprintln("First line");
+ /// result.eprintln("Second line");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn eprintln(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stderr);
}
/// Clears all rendered content.
@@ -174,7 +357,148 @@ impl RenderResult {
/// assert!(result.is_empty());
/// ```
pub fn clear(&mut self) {
- self.render_text.clear();
+ self.render_buffer.clear();
+ }
+
+ /// Outputs all buffered content to stdout and stderr according to their respective modes.
+ ///
+ /// Iterates through the render buffer and prints each buffered string to the appropriate
+ /// output stream — stdout for `Stdout` entries and stderr for `Stderr` entries.
+ ///
+ /// This method is typically used to flush the buffered output at the end of rendering,
+ /// ensuring that all output is displayed in the correct order and to the correct stream.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error", RenderResultMode::Stderr);
+ /// result.std_print(); // prints "Hello" to stdout and "Error" to stderr
+ /// ```
+ pub fn std_print(&self) {
+ for (content, mode) in self.render_buffer.iter() {
+ match mode {
+ Stdout => print!("{}", content),
+ Stderr => eprint!("{}", content),
+ }
+ }
+ }
+
+ /// Returns the total number of characters (in terms of `char` count) in the buffered render output.
+ ///
+ /// This counts the length across all buffered entries, regardless of whether they are
+ /// destined for stdout or stderr.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print("Hello");
+ /// result.print(", 世界");
+ /// assert_eq!(result.len(), 9); // "Hello, 世界" has 9 chars
+ /// ```
+ pub fn len(&self) -> usize {
+ self.render_buffer
+ .iter()
+ .map(|(s, _)| s.chars().count())
+ .sum()
+ }
+
+ /// Returns `true` if the buffered render output contains no characters.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// assert!(result.is_empty());
+ /// result.print("Hello");
+ /// assert!(!result.is_empty());
+ /// ```
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Trims leading and trailing whitespace from the buffered render output.
+ ///
+ /// This method processes the render buffer as follows:
+ /// - If the buffer is empty, it returns `self` unchanged.
+ /// - If there is only one entry, whitespace is trimmed from both the start and end of that
+ /// single entry.
+ /// - If there are multiple entries, whitespace is trimmed from the start of the first entry
+ /// and the end of the last entry.
+ ///
+ /// Whitespace in the middle entries is preserved. This is useful for cleaning up output
+ /// without removing intentional spacing between separately buffered segments.
+ ///
+ /// # Returns
+ ///
+ /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with
+ /// trimmed text content.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print(" Hello, world! ");
+ /// let trimmed = result.trim_buffer();
+ /// assert_eq!(trimmed.to_string().trim(), "Hello, world!");
+ /// ```
+ pub fn trim_buffer(self) -> RenderResult {
+ if self.render_buffer.is_empty() {
+ return self;
+ }
+
+ let mut buffer = self.render_buffer;
+ if buffer.len() == 1 {
+ // Only one entry: trim both start and end of this single entry
+ let (text, mode) = buffer.remove(0);
+ buffer.push((text.trim().to_string(), mode));
+ } else {
+ // Multiple entries: trim start of first, trim end of last
+ let first_len = buffer.len();
+
+ // Trim start of first entry
+ let (first_text, first_mode) = buffer.remove(0);
+ let trimmed_first = first_text.trim_start().to_string();
+ buffer.insert(0, (trimmed_first, first_mode));
+
+ // Trim end of last entry
+ let (last_text, last_mode) = buffer.remove(first_len - 1);
+ let trimmed_last = last_text.trim_end().to_string();
+ buffer.push((trimmed_last, last_mode));
+ }
+
+ RenderResult {
+ render_buffer: buffer,
+ immediate_output: self.immediate_output,
+ exit_code: self.exit_code,
+ }
+ }
+}
+
+#[inline(always)]
+fn render_result_to_string(result: &RenderResult) -> String {
+ let mut buffer = String::new();
+ for item in result.render_buffer.iter() {
+ buffer += &item.0;
+ }
+ buffer
+}
+
+#[inline(always)]
+fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) -> RenderResult {
+ RenderResult {
+ render_buffer: vec![(string.into(), mode)],
+ ..Default::default()
}
}
@@ -191,20 +515,6 @@ mod tests {
}
#[test]
- fn print_appends_text() {
- let mut result = RenderResult::default();
- result.print("Hello");
- assert_eq!(result.deref(), "Hello");
- }
-
- #[test]
- fn println_appends_text_with_newline() {
- let mut result = RenderResult::default();
- result.println("Hello");
- assert_eq!(result.deref(), "Hello\n");
- }
-
- #[test]
fn clear_empties_content() {
let mut result = RenderResult::default();
result.print("something");
@@ -222,14 +532,6 @@ mod tests {
}
#[test]
- fn write_appends_utf8_bytes() {
- let mut result = RenderResult::default();
- let n = IoWrite::write(&mut result, b"hello").unwrap();
- assert_eq!(n, 5);
- assert_eq!(result.deref(), "hello");
- }
-
- #[test]
fn write_with_invalid_utf8_returns_error() {
let mut result = RenderResult::default();
let err = IoWrite::write(&mut result, &[0xff, 0xfe]).unwrap_err();
@@ -241,16 +543,7 @@ mod tests {
let mut result = RenderResult::default();
result.print(" hello world \n");
let formatted = format!("{}", result);
- assert_eq!(formatted, "hello world\n");
- }
-
- #[test]
- fn deref_exposes_inner_text_as_str() {
- let mut result = RenderResult::default();
- result.print("test");
-
- let s: &str = &result;
- assert_eq!(s, "test");
+ assert_eq!(formatted, "hello world");
}
#[test]
@@ -270,4 +563,72 @@ mod tests {
// original is still usable
assert!(!result.is_empty());
}
+
+ #[test]
+ fn trim_empty_buffer_returns_self() {
+ let result = RenderResult::default();
+ let trimmed = result.trim_buffer();
+ assert!(trimmed.is_empty());
+ assert_eq!(trimmed.exit_code, 0);
+ }
+
+ #[test]
+ fn trim_single_entry_trims_both_ends() {
+ let mut result = RenderResult::default();
+ result.print(" Hello, world! ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello, world!");
+ }
+
+ #[test]
+ fn trim_single_entry_nothing_to_trim() {
+ let mut result = RenderResult::default();
+ result.print("Hello");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_multiple_entries_trims_first_start_and_last_end() {
+ let mut result = RenderResult::default();
+ result.print(" Hello");
+ result.print(" World ");
+ result.print("! ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: "Hello"
+ // middle entry unchanged: " World "
+ // last entry trim_end: "!"
+ assert_eq!(trimmed.to_string(), "Hello World !");
+ }
+
+ #[test]
+ fn trim_multiple_entries_only_whitespace_first_entry() {
+ let mut result = RenderResult::default();
+ result.print(" ");
+ result.print("Hello");
+ result.print(" ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: ""
+ // middle entry unchanged: "Hello"
+ // last entry trim_end: ""
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_preserves_exit_code() {
+ let mut result = RenderResult::new();
+ result.exit_code = 42;
+ result.print(" test ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.exit_code, 42);
+ }
+
+ #[test]
+ fn trim_preserves_stderr_mode() {
+ let mut result = RenderResult::default();
+ result.eprint(" error ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr);
+ assert_eq!(trimmed.to_string(), "error");
+ }
}
diff --git a/mingling_core/src/renderer/structural.rs b/mingling_core/src/renderer/structural.rs
index 30255aa..0449e72 100644
--- a/mingling_core/src/renderer/structural.rs
+++ b/mingling_core/src/renderer/structural.rs
@@ -1,5 +1,5 @@
use crate::{
- RenderResult, StructuralRendererSetting,
+ ProgramCollect, RenderResult, StructuralRendererSetting,
renderer::structural::error::StructuralRendererSerializeError,
};
use serde::Serialize;
@@ -24,11 +24,15 @@ impl StructuralRenderer {
///
/// Returns `Err(StructuralRendererSerializeError)` if serialization fails.
#[allow(unused_variables)]
- pub fn render<T: StructuralData + Send>(
+ pub fn render<T, C>(
data: &T,
setting: &StructuralRendererSetting,
r: &mut RenderResult,
- ) -> Result<(), StructuralRendererSerializeError> {
+ ) -> Result<(), StructuralRendererSerializeError>
+ where
+ T: StructuralData<C> + Send,
+ C: ProgramCollect<Enum = C>,
+ {
match setting {
StructuralRendererSetting::Disable => Ok(()),
#[cfg(feature = "json_serde_fmt")]
@@ -148,7 +152,7 @@ impl StructuralRenderer {
#[cfg(test)]
mod tests {
use super::*;
- use crate::RenderResult;
+ use crate::{MockProgramCollect, RenderResult};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
@@ -157,8 +161,8 @@ mod tests {
value: i32,
}
- impl crate::__private::StructuralDataSealed for TestData {}
- impl StructuralData for TestData {}
+ impl crate::__private::StructuralDataSealed<MockProgramCollect> for TestData {}
+ impl StructuralData<MockProgramCollect> for TestData {}
fn test_data() -> TestData {
TestData {
diff --git a/mingling_core/src/renderer/structural/structural_data.rs b/mingling_core/src/renderer/structural/structural_data.rs
index 1cafac3..583808c 100644
--- a/mingling_core/src/renderer/structural/structural_data.rs
+++ b/mingling_core/src/renderer/structural/structural_data.rs
@@ -1,5 +1,7 @@
use serde::Serialize;
+use crate::ProgramCollect;
+
/// Marker trait for types that support structured output (JSON / YAML / TOML / RON).
///
/// This trait is a **supertrait** of `serde::Serialize` and is sealed via
@@ -12,4 +14,8 @@ use serde::Serialize;
/// These entry points also register the type in the global `STRUCTURED_TYPES`
/// registry, which is required for the `structural_render` match arm to be generated.
#[doc(hidden)]
-pub trait StructuralData: Serialize + crate::__private::StructuralDataSealed {}
+pub trait StructuralData<C>: Serialize + crate::__private::StructuralDataSealed<C>
+where
+ C: ProgramCollect<Enum = C>,
+{
+}