diff options
Diffstat (limited to 'mingling_core')
| -rw-r--r-- | mingling_core/src/any.rs | 122 | ||||
| -rw-r--r-- | mingling_core/src/any/group.rs | 12 | ||||
| -rw-r--r-- | mingling_core/src/asset/dispatcher.rs | 6 | ||||
| -rw-r--r-- | mingling_core/src/asset/global_resource.rs | 6 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 8 | ||||
| -rw-r--r-- | mingling_core/src/program/collection/mock.rs | 7 | ||||
| -rw-r--r-- | mingling_core/src/program/hook.rs | 15 | ||||
| -rw-r--r-- | mingling_core/src/program/once_exec.rs | 38 | ||||
| -rw-r--r-- | mingling_core/src/program/setup.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/renderer/render_result.rs | 497 | ||||
| -rw-r--r-- | mingling_core/src/renderer/structural.rs | 16 | ||||
| -rw-r--r-- | mingling_core/src/renderer/structural/structural_data.rs | 8 | ||||
| -rw-r--r-- | mingling_core/tests/test-all/tests/integration.rs | 17 | ||||
| -rw-r--r-- | mingling_core/tests/test-basic/tests/integration.rs | 2 | ||||
| -rw-r--r-- | mingling_core/tests/test-dispatch-tree/tests/integration.rs | 2 | ||||
| -rw-r--r-- | mingling_core/tests/test-repl/tests/integration.rs | 2 | ||||
| -rw-r--r-- | mingling_core/tests/test-structural-renderer/tests/integration.rs | 8 |
17 files changed, 630 insertions, 139 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>, +{ +} diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs index 3581702..d36b9df 100644 --- a/mingling_core/tests/test-all/tests/integration.rs +++ b/mingling_core/tests/test-all/tests/integration.rs @@ -1,13 +1,12 @@ use mingling::Flag; -use mingling::StructuralRenderer; -use mingling::StructuralRendererSetting; -use mingling::MockProgramCollect; use mingling::NextProcess; -use mingling::StructuralData; use mingling::Node; use mingling::Program; use mingling::RenderResult; use mingling::StringVec; +use mingling::StructuralData; +use mingling::StructuralRenderer; +use mingling::StructuralRendererSetting; use mingling::comp::{ShellContext, ShellFlag, Suggest}; use mingling::core_res::ResREPL; use mingling::hook::ProgramHook; @@ -86,7 +85,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } // StructuralRenderer @@ -125,13 +124,13 @@ fn test_structural_renderer_json() { #[test] fn test_is_completing() { - let program: Program<MockProgramCollect> = Program::new_with_args(["app", "__comp"]); + let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "__comp"]); assert!(program.is_completing()); } #[test] fn test_is_not_completing() { - let program: Program<MockProgramCollect> = Program::new_with_args(["app", "greet"]); + let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "greet"]); assert!(!program.is_completing()); } @@ -141,7 +140,7 @@ fn test_is_not_completing() { fn test_hook_setup() { static CALLED: AtomicBool = AtomicBool::new(false); - let hook = ProgramHook::<MockProgramCollect>::empty().on_begin::<_, ()>(|_| { + let hook = ProgramHook::<crate::ThisProgram>::empty().on_begin::<_, ()>(|_| { CALLED.store(true, Ordering::SeqCst); }); @@ -166,3 +165,5 @@ fn test_string_vec_from_array() { let v: Vec<String> = sv.into(); assert_eq!(v, vec!["a", "b", "c"]); } + +mingling::macros::gen_program!(); diff --git a/mingling_core/tests/test-basic/tests/integration.rs b/mingling_core/tests/test-basic/tests/integration.rs index 7cd7b8c..e51992c 100644 --- a/mingling_core/tests/test-basic/tests/integration.rs +++ b/mingling_core/tests/test-basic/tests/integration.rs @@ -60,7 +60,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } #[test] diff --git a/mingling_core/tests/test-dispatch-tree/tests/integration.rs b/mingling_core/tests/test-dispatch-tree/tests/integration.rs index 7cd7b8c..e51992c 100644 --- a/mingling_core/tests/test-dispatch-tree/tests/integration.rs +++ b/mingling_core/tests/test-dispatch-tree/tests/integration.rs @@ -60,7 +60,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } #[test] diff --git a/mingling_core/tests/test-repl/tests/integration.rs b/mingling_core/tests/test-repl/tests/integration.rs index 1792525..4de0e8f 100644 --- a/mingling_core/tests/test-repl/tests/integration.rs +++ b/mingling_core/tests/test-repl/tests/integration.rs @@ -59,5 +59,5 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } diff --git a/mingling_core/tests/test-structural-renderer/tests/integration.rs b/mingling_core/tests/test-structural-renderer/tests/integration.rs index 3c3c6db..e4057f8 100644 --- a/mingling_core/tests/test-structural-renderer/tests/integration.rs +++ b/mingling_core/tests/test-structural-renderer/tests/integration.rs @@ -1,4 +1,4 @@ -use mingling::{StructuralRenderer, StructuralRendererSetting, RenderResult, StructuralData}; +use mingling::{RenderResult, StructuralData, StructuralRenderer, StructuralRendererSetting}; use serde::Serialize; #[derive(Debug, Clone, PartialEq, Serialize, StructuralData)] @@ -17,7 +17,8 @@ fn test_data() -> TestData { #[test] fn test_render_disable() { let mut r = RenderResult::default(); - let result = StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r); + let result = + StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r); assert!(result.is_ok()); assert!(r.is_empty()); } @@ -73,3 +74,6 @@ fn test_render_ron() { assert!(output.contains("value:")); assert!(output.contains("42")); } + +mingling::macros::gen_program!(); + |
