diff options
Diffstat (limited to 'mingling_core/src')
27 files changed, 1268 insertions, 645 deletions
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index e6b7406..e922e2e 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -1,8 +1,8 @@ +use crate::ProgramCollect; use crate::error::ChainProcessError; -use crate::{Grouped, ProgramCollect}; -#[doc(hidden)] -pub mod group; +mod group; +pub use group::*; /// Any type output /// @@ -17,8 +17,19 @@ pub mod group; #[derive(Debug)] pub struct AnyOutput<G> { pub(crate) inner: Box<dyn std::any::Any + Send + 'static>, - pub type_id: std::any::TypeId, - pub member_id: G, + + /// The [`TypeId`] of the concrete type stored in `inner`. + /// + /// This is set during construction and used for type-checking + /// in downcast, restore, and is methods. + 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(crate) member_id: G, } impl<G> AnyOutput<G> { @@ -34,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 @@ -106,7 +163,9 @@ impl<G> std::ops::DerefMut for AnyOutput<G> { /// - Returns `Ok((`[`AnyOutput`](./struct.AnyOutput.html)`, `[`NextProcess::Renderer`](./enum.NextProcess.html)`))` to render this type next and output to the terminal /// - Returns `Err(`[`ChainProcessError`](./error/enum.ChainProcessError.html)`]` to terminate the program directly pub enum ChainProcess<G> { + /// Indicates success, containing the output value and the next step to execute. Ok((AnyOutput<G>, NextProcess)), + /// Indicates a processing failure, containing the error that occurred. Err(ChainProcessError), } @@ -117,7 +176,9 @@ pub enum ChainProcess<G> { #[derive(Debug, PartialEq, Eq)] #[repr(u8)] pub enum NextProcess { + /// Continue execution to the next chain Chain, + /// Send output to renderer and end execution Renderer, } @@ -175,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 } @@ -187,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 } @@ -198,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 } @@ -354,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 } @@ -381,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 90ef9fc..5e5e347 100644 --- a/mingling_core/src/any/group.rs +++ b/mingling_core/src/any/group.rs @@ -2,35 +2,19 @@ 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, { /// Returns the specific enum value representing its ID within that enum fn member_id() -> Group; - - /// Converts the grouped item into a `ChainProcess` directed to the chain route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. - fn to_chain(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_chain() - } - - /// Converts the grouped item into a `ChainProcess` directed to the render route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. - fn to_render(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_renderer() - } } impl<T, C> Routable<C> for T @@ -39,10 +23,10 @@ where T: Grouped<C> + Send, { fn to_chain(self) -> ChainProcess<C> { - T::to_chain(self) + AnyOutput::new(self).route_chain() } fn to_render(self) -> ChainProcess<C> { - T::to_render(self) + AnyOutput::new(self).route_renderer() } } diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index 9b9a5d4..8c709ac 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -1,26 +1,10 @@ -#[doc(hidden)] -pub mod chain; - -#[doc(hidden)] -pub mod dispatcher; - -#[doc(hidden)] -pub mod enum_tag; - -#[doc(hidden)] -pub mod global_resource; - -#[doc(hidden)] -pub mod help; - -#[doc(hidden)] -pub mod lazy_resource; - -#[doc(hidden)] -pub mod node; - -#[doc(hidden)] -pub mod renderer; - -#[doc(hidden)] -pub mod routable; +pub(crate) mod chain; +pub(crate) mod core_invokes; +pub(crate) mod dispatcher; +pub(crate) mod enum_tag; +pub(crate) mod global_resource; +pub(crate) mod help; +pub(crate) mod lazy_resource; +pub(crate) mod node; +pub(crate) mod renderer; +pub(crate) mod routable; diff --git a/mingling_core/src/asset/chain.rs b/mingling_core/src/asset/chain.rs index 423e218..bd504d0 100644 --- a/mingling_core/src/asset/chain.rs +++ b/mingling_core/src/asset/chain.rs @@ -12,6 +12,7 @@ pub trait Chain<G> { #[cfg(feature = "async")] fn proc(p: Self::Previous) -> impl Future<Output = ChainProcess<G>> + Send; + /// Process the previous value and return a future that resolves to a [`ChainProcess<G>`](./enum.ChainProcess.html) #[cfg(not(feature = "async"))] fn proc(p: Self::Previous) -> ChainProcess<G>; } diff --git a/mingling_core/src/asset/core_invokes.rs b/mingling_core/src/asset/core_invokes.rs new file mode 100644 index 0000000..6a3da2d --- /dev/null +++ b/mingling_core/src/asset/core_invokes.rs @@ -0,0 +1,271 @@ +use std::marker::PhantomData; + +use crate::{ + AnyOutput, ChainProcess, Grouped, NextProcess, ProgramCollect, RenderResult, ResourceMarker, +}; + +/// Type used to invoke Renderers in Mingling +/// +/// It is marked as `#[non_exhaustive]` and all internal fields are private. +/// This type can only be implicitly created by the hidden external API +/// `__resource_marker_default` of `ResourceMarker`. +/// +/// ```rust,ignore +/// // You can inject other renderers as types into the current function +/// // | +/// #[renderer(buffer)] // vvvvvvvvvvvvvvvvvvvv +/// fn render_foo(_: ResultFoo, renderer: &RendererInvoker<Bar>) { +/// let bar = Bar::default(); +/// r_append!(renderer.invoke(bar)); +/// } +/// ``` +#[non_exhaustive] +pub struct RendererInvoker<T> { + phantom: PhantomData<T>, + create_by_res_injection: bool, +} + +impl<T> RendererInvoker<T> +where + T: Send, +{ + /// Invoke the renderer with the given value. + /// + /// This function triggers the rendering pipeline for the provided value `T`. + /// It can only be executed when the `RendererInvoker` was created by the resource injection system + /// (i.e., via `__resource_marker_default`). If the invoker was created manually or cloned outside + /// the injection context, calling this method will panic. + /// + /// # Panics + /// + /// Panics if the `RendererInvoker` was not created by the resource injection system. + /// + /// # Hook + /// + /// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control. + pub fn invoke<C>(&self, value: T) -> RenderResult + where + C: ProgramCollect<Enum = C> + 'static, + T: Grouped<C>, + { + if !self.create_by_res_injection { + panic!( + "The current RendererInvoker was not created by the resource injection system, so it cannot be executed!" + ); + } + C::render(AnyOutput::new(value)) + } +} + +impl<T> ResourceMarker for RendererInvoker<T> { + fn __resource_marker_clone(&self) -> Self { + RendererInvoker { + phantom: PhantomData, + create_by_res_injection: self.create_by_res_injection, + } + } + + fn __resource_marker_default() -> Self { + // Create RendererInvoker with `create_by_res_injection` marked as true + // + // Reason: RendererInvoker is designed to only be created through resource injection. + // When the resource injection does not find a corresponding value, + // this method will be used to generate a default value to pass in. + RendererInvoker { + phantom: PhantomData, + create_by_res_injection: true, + } + } + + fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self)) + where + C: ProgramCollect<Enum = C> + 'static, + { + // DO NOTHING + // + // When ResourceMarker is asked to modify, it should not execute anything. + let _ = f; + } +} + +/// Type used to invoke Chain in Mingling +/// +/// It is marked as `#[non_exhaustive]` and all internal fields are private. +/// This type can only be implicitly created by the hidden external API +/// `__resource_marker_default` of `ResourceMarker`. +/// +/// ```rust,ignore +/// // You can inject other chain as types into the current function +/// // | +/// #[chain] // vvvvvvvvvvvvvvvvvvvvv +/// fn handle_foo(_: EntryFoo, chain: &ChainInvoker<StateBar>) { +/// let bar = Bar::default(); +/// let next = chain.invoke_once(bar); +/// } +/// ``` +#[non_exhaustive] +pub struct ChainInvoker<T> { + phantom: PhantomData<T>, + create_by_res_injection: bool, +} + +impl<T> ChainInvoker<T> +where + T: Send, +{ + /// Execute one step of the chain with the given value, returning the next state. + /// + /// This function performs **only a single step** of chain execution — it invokes the + /// current handler for value `T` and returns the next [`ChainProcess`] state, without + /// automatically continuing the chain. The caller is responsible for proceeding through + /// subsequent steps based on the returned state. + /// + /// It can only be executed when the `ChainInvoker` was created by the resource injection system + /// (i.e., via `__resource_marker_default`). If the invoker was created manually or cloned outside + /// the injection context, calling this method will panic. + /// + /// # Special Behavior + /// + /// If no chain exists for this type, it will convert itself into a `ChainProcess` that routes to the chain and return it. + /// + /// # Panics + /// + /// Panics if the `ChainInvoker` was not created by the resource injection system. + /// + /// # Hooks + /// + /// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control. + #[might_be_async::func] + pub fn invoke_once<C>(&self, value: T) -> ChainProcess<C> + where + C: ProgramCollect<Enum = C> + 'static, + T: Grouped<C>, + { + self.pre_check(); + + let any = AnyOutput::new(value); + + if C::has_chain(&any) { + might_be_async::invoke!(C::do_chain(any)) + } else { + ChainProcess::Ok((any, NextProcess::Chain)) + } + } + + /// Continuously execute the chain until it is routed to a renderer or can no longer continue + /// + /// This function can only be called when the `ChainInvoker` was created by the resource injection system + /// (i.e., via `__resource_marker_default`). If the invoker was created manually or cloned outside + /// the injection context, calling this method will panic. + /// + /// # Panics + /// + /// Panics if the `ChainInvoker` was not created by the resource injection system. + /// + /// # Hooks + /// + /// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control. + #[might_be_async::func] + pub fn invoke_to_last<C>(&self, value: T) -> ChainProcess<C> + where + C: ProgramCollect<Enum = C> + 'static, + T: Grouped<C>, + { + self.pre_check(); + + let mut current = might_be_async::invoke!(C::do_chain(AnyOutput::new(value))); + + loop { + match current { + ChainProcess::Ok((any, NextProcess::Chain)) => { + if C::has_chain(&any) { + current = might_be_async::invoke!(C::do_chain(any)); + } else { + // If the next step of this type does not have a Chain, reconstruct it back + return ChainProcess::Ok((any, NextProcess::Chain)); + } + } + _ => return current, + } + } + } + + /// Continuously execute the chain until it is rendered into a RenderResult + /// + /// This function can only be called when the `ChainInvoker` was created by the resource injection system + /// (i.e., via `__resource_marker_default`). If the invoker was created manually or cloned outside + /// the injection context, calling this method will panic. + /// + /// # Special Behavior + /// + /// If an error occurs during rendering, or the result type does not contain a renderer, it will be rendered as an empty RenderResult + /// + /// # Panics + /// + /// Panics if the `ChainInvoker` was not created by the resource injection system. + /// + /// # Hooks + /// + /// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control. + #[might_be_async::func] + pub fn invoke_to_result<C>(&self, value: T) -> RenderResult + where + C: ProgramCollect<Enum = C> + 'static, + T: Grouped<C>, + { + self.pre_check(); + + let last = might_be_async::invoke!(self.invoke_to_last(value)); + + match last { + ChainProcess::Err(_) => RenderResult::new(), + ChainProcess::Ok((any, _)) => { + if C::has_renderer(&any) { + C::render(any) + } else { + RenderResult::new() + } + } + } + } + + #[inline(always)] + fn pre_check(&self) { + if !self.create_by_res_injection { + panic!( + "The current ChainInvoker was not created by the resource injection system, so it cannot be executed!" + ); + } + } +} + +impl<T> ResourceMarker for ChainInvoker<T> { + fn __resource_marker_clone(&self) -> Self { + ChainInvoker { + phantom: PhantomData, + create_by_res_injection: self.create_by_res_injection, + } + } + + fn __resource_marker_default() -> Self { + // Create ChainInvoker with `create_by_res_injection` marked as true + // + // Reason: ChainInvoker is designed to only be created through resource injection. + // When the resource injection does not find a corresponding value, + // this method will be used to generate a default value to pass in. + ChainInvoker { + phantom: PhantomData, + create_by_res_injection: true, + } + } + + fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self)) + where + C: ProgramCollect<Enum = C> + 'static, + { + // DO NOTHING + // + // When ResourceMarker is asked to modify, it should not execute anything. + let _ = f; + } +} diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs index 8f04955..cb0987d 100644 --- a/mingling_core/src/asset/dispatcher.rs +++ b/mingling_core/src/asset/dispatcher.rs @@ -32,22 +32,48 @@ where C: ProgramCollect<Enum = C>, { /// Adds a dispatcher to the program. - #[cfg(not(feature = "dispatch_tree"))] - pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp) + #[cfg_attr( + feature = "dispatch_tree", + deprecated( + 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) -> &mut Self where Disp: Dispatcher<C> + Send + Sync + 'static, { - self.dispatcher.push(Box::new(dispatcher)); + #[cfg(not(feature = "dispatch_tree"))] + { + self.dispatcher.push(Box::new(dispatcher)); + } + #[cfg(feature = "dispatch_tree")] + { + let _ = dispatcher; + } + self } /// Add some dispatchers to the program. - #[cfg(not(feature = "dispatch_tree"))] - pub fn with_dispatchers<D>(&mut self, dispatchers: D) + #[cfg_attr( + feature = "dispatch_tree", + deprecated( + 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) -> &mut Self where D: Into<Dispatchers<C>>, { - let dispatchers = dispatchers.into(); - self.dispatcher.extend(dispatchers.dispatcher); + #[cfg(not(feature = "dispatch_tree"))] + { + let dispatchers = dispatchers.into(); + self.dispatcher.extend(dispatchers.dispatcher); + } + #[cfg(feature = "dispatch_tree")] + { + let _ = dispatchers; + } + self } } diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs index 29e1136..18e4446 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 @@ -34,7 +38,7 @@ where { let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) { Ok(val) => val, - Err(arc) => (*arc).res_clone(), + Err(arc) => (*arc).__resource_marker_clone(), }; let r = f(&mut new_res); *arc_res = Arc::new(new_res); @@ -53,7 +57,7 @@ where Res: 'static + Default + ResourceMarker + Send + Sync, { let Ok(mut guard) = self.resources.lock() else { - let mut default_res = Res::res_default(); + let mut default_res = Res::__resource_marker_default(); return f(&mut default_res); }; if let Some(arc_res) = guard @@ -62,13 +66,13 @@ where { let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) { Ok(val) => val, - Err(arc) => (*arc).res_clone(), + Err(arc) => (*arc).__resource_marker_clone(), }; let r = f(&mut new_res); *arc_res = Arc::new(new_res); r } else { - let mut default_res = Res::res_default(); + let mut default_res = Res::__resource_marker_default(); f(&mut default_res) } } @@ -81,7 +85,7 @@ where #[must_use] pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res { let Ok(mut guard) = self.resources.lock() else { - return Res::res_default(); + return Res::__resource_marker_default(); }; if let Some(arc_res) = guard .get_mut(&TypeId::of::<Res>()) @@ -89,10 +93,10 @@ where { match Arc::try_unwrap(std::mem::take(arc_res)) { Ok(val) => val, - Err(arc) => (*arc).res_clone(), + Err(arc) => (*arc).__resource_marker_clone(), } } else { - Res::res_default() + Res::__resource_marker_default() } } @@ -132,7 +136,7 @@ where &self, ) -> GlobalResource<Res> { self.res() - .unwrap_or_else(|| GlobalResource::from(Arc::new(Res::res_default()))) + .unwrap_or_else(|| GlobalResource::from(Arc::new(Res::__resource_marker_default()))) } } @@ -172,24 +176,38 @@ impl<ResType: 'static + Send + Sync> AsRef<ResType> for GlobalResource<ResType> /// Resource marker trait, types that implement the Clone and Default traits can be considered as resources pub trait ResourceMarker { + /// Clone the resource. This is an internal method used by the resource injection system + /// and should not be called directly by user code. #[must_use] - fn res_clone(&self) -> Self; - fn res_default() -> Self; - fn modify<C>(f: impl FnOnce(&mut Self)) + #[doc(hidden)] + fn __resource_marker_clone(&self) -> Self; + + /// Create a default instance of the resource. This is an internal method used by the + /// resource injection system and should not be called directly by user code. + #[doc(hidden)] + fn __resource_marker_default() -> Self; + + /// Modify the resource using a closure. This is an internal method used by the resource + /// injection system and should not be called directly by user code. + #[doc(hidden)] + fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self)) where C: ProgramCollect<Enum = C> + 'static; } -impl<T: Default + Clone + Send + Sync + 'static> ResourceMarker for T { - fn res_clone(&self) -> Self { +impl<T> ResourceMarker for T +where + T: Default + Clone + Send + Sync + 'static, +{ + fn __resource_marker_clone(&self) -> Self { Clone::clone(self) } - fn res_default() -> Self { + fn __resource_marker_default() -> Self { Default::default() } - fn modify<C>(f: impl FnOnce(&mut Self)) + fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self)) where C: ProgramCollect<Enum = C> + 'static, { @@ -223,38 +241,41 @@ mod tests { #[test] fn resource_marker_i32_res_clone() { let val = 42i32; - let cloned = val.res_clone(); + let cloned = val.__resource_marker_clone(); assert_eq!(cloned, 42); } #[test] fn resource_marker_i32_res_default() { - assert_eq!(<i32 as ResourceMarker>::res_default(), 0i32); + assert_eq!(<i32 as ResourceMarker>::__resource_marker_default(), 0i32); } #[test] fn resource_marker_string_res_clone() { let val = "hello".to_string(); - let cloned = val.res_clone(); + let cloned = val.__resource_marker_clone(); assert_eq!(cloned, "hello"); } #[test] fn resource_marker_string_res_default() { - assert_eq!(<String as ResourceMarker>::res_default(), ""); + assert_eq!(<String as ResourceMarker>::__resource_marker_default(), ""); } #[test] fn resource_marker_vec_res_clone() { let val = vec![1, 2, 3]; - let cloned = val.res_clone(); + let cloned = val.__resource_marker_clone(); assert_eq!(cloned, vec![1, 2, 3]); } #[test] fn resource_marker_vec_res_default() { let empty: Vec<i32> = vec![]; - assert_eq!(<Vec<i32> as ResourceMarker>::res_default(), empty); + assert_eq!( + <Vec<i32> as ResourceMarker>::__resource_marker_default(), + empty + ); } // Note: Tests for Program::with_resource, res(), res_or_route(), res_or_default(), diff --git a/mingling_core/src/asset/lazy_resource.rs b/mingling_core/src/asset/lazy_resource.rs index 918aeb2..3cb7563 100644 --- a/mingling_core/src/asset/lazy_resource.rs +++ b/mingling_core/src/asset/lazy_resource.rs @@ -280,7 +280,7 @@ where { /// Clones the lazy resource. The cloned resource retains any initialized value, /// but the initializer is reset to `T::default()`. - fn res_clone(&self) -> Self { + fn __resource_marker_clone(&self) -> Self { match &self.inner { LazyInner::Init(t, _) => Self { inner: LazyInner::Init(t.clone(), None), @@ -290,7 +290,7 @@ where } /// Returns a default lazy resource (uninitialized, using `T::default()` as the initializer). - fn res_default() -> Self + fn __resource_marker_default() -> Self where T: Default, { @@ -298,7 +298,7 @@ where } /// Modifies the current lazy resource via the `this` context provided by `C`. - fn modify<C>(f: impl FnOnce(&mut Self)) + fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self)) where C: ProgramCollect<Enum = C> + 'static, { @@ -583,7 +583,7 @@ mod tests { fn res_clone_of_initialized_clones_value() { let mut r = LazyRes::new(|| vec![1, 2, 3]); r.get_ref(); - let cloned = r.res_clone(); + let cloned = r.__resource_marker_clone(); assert!(cloned.is_initialized()); assert_eq!(cloned.into_inner(), Some(vec![1, 2, 3])); } @@ -591,14 +591,14 @@ mod tests { #[test] fn res_clone_of_uninitialized_creates_default() { let r: LazyRes<Vec<i32>> = LazyRes::new(|| vec![1, 2, 3]); - let cloned = r.res_clone(); + let cloned = r.__resource_marker_clone(); // The source is uninitialized, so res_clone returns a default lazy assert!(!cloned.is_initialized()); } #[test] fn res_default_returns_uninitialized() { - let r: LazyRes<i32> = LazyRes::<i32>::res_default(); + let r: LazyRes<i32> = LazyRes::<i32>::__resource_marker_default(); assert!(!r.is_initialized()); } diff --git a/mingling_core/src/build.rs b/mingling_core/src/build.rs new file mode 100644 index 0000000..7918f3a --- /dev/null +++ b/mingling_core/src/build.rs @@ -0,0 +1,13 @@ +#[doc(hidden)] +#[cfg(feature = "comp")] +mod comp; + +#[cfg(feature = "comp")] +pub use comp::*; + +#[doc(hidden)] +#[cfg(feature = "pathf")] +mod pathf; + +#[cfg(feature = "pathf")] +pub use pathf::*; diff --git a/mingling_core/src/builds/comp.rs b/mingling_core/src/build/comp.rs index 2abb1e1..2abb1e1 100644 --- a/mingling_core/src/builds/comp.rs +++ b/mingling_core/src/build/comp.rs diff --git a/mingling_core/src/builds/pathf.rs b/mingling_core/src/build/pathf.rs index d8d4698..d8d4698 100644 --- a/mingling_core/src/builds/pathf.rs +++ b/mingling_core/src/build/pathf.rs diff --git a/mingling_core/src/builds.rs b/mingling_core/src/builds.rs deleted file mode 100644 index 3c52907..0000000 --- a/mingling_core/src/builds.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[doc(hidden)] -#[cfg(feature = "comp")] -pub mod comp; - -#[doc(hidden)] -#[cfg(feature = "pathf")] -pub mod pathf; diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs index 55e9952..d8dcfbd 100644 --- a/mingling_core/src/comp.rs +++ b/mingling_core/src/comp.rs @@ -34,7 +34,17 @@ use crate::exec::match_user_input; /// Types implementing this trait can provide custom completion suggestions /// based on the current shell context. pub trait Completion { + /// The entry point type that the completion functionality will act on. + /// + /// It marks the **previous** type, which typically represents an `EntryXXX` type + /// (unless you have specific requirements). type Previous; + + /// Generates completion suggestions based on the current shell context. + /// + /// This method is called when the completion system needs to provide + /// custom suggestions for the current command or argument. Implementations + /// should use the provided [`ShellContext`] to determine what to suggest. fn comp(ctx: &ShellContext) -> Suggest; } @@ -44,6 +54,18 @@ pub trait Completion { /// automatically implement this trait for `Entry` types to extract the /// arguments from user input for completion suggestions. pub trait CompletionEntry { + /// Extracts the user input arguments for completion processing. + /// + /// This method is called when the completion system needs to retrieve + /// the raw input arguments from the user's command line. The returned + /// vector of strings represents the parsed arguments that will be used + /// to match against the program's command tree and generate completion + /// suggestions. + /// + /// # Returns + /// + /// A vector of strings containing the individual arguments from the + /// user's command-line input. fn get_input(self) -> Vec<String>; } @@ -54,6 +76,27 @@ pub trait CompletionEntry { /// format appropriate for the target shell. pub struct CompletionHelper; impl CompletionHelper { + /// Executes the completion logic for the given program type (`P`). + /// + /// This is the **entry point** of the Mingling Completion system. It converts + /// the user's shell context (current command line, cursor position, previous + /// words, etc.) into actual completion suggestions. + /// + /// The function first attempts to match the input arguments against the + /// program's command tree. If a matching dispatcher is found, it delegates to + /// the program's custom completion logic (`P::do_comp`). If no match is found + /// or the dispatcher signals "not found", a default completion is used instead, + /// which provides subcommand suggestions based on the input path. + /// + /// # Type Parameters + /// + /// * `P` — The top-level program type that implements [`ProgramCollect`], can be + /// displayed, compared for equality, and kept alive for the program's lifetime. + /// + /// # Returns + /// + /// A [`Suggest`] value containing either file completion instructions or a set + /// of candidate suggestions. #[must_use] pub fn exec_completion<P>(ctx: &ShellContext) -> Suggest where @@ -126,7 +169,17 @@ impl CompletionHelper { } } - #[allow(clippy::needless_pass_by_value)] + /// Renders the completion suggestions to standard output. + /// + /// This method takes the [`Suggest`] value produced by + /// [`exec_completion`], and prints the results in a format appropriate for the + /// user's shell environment. + /// + /// - If the suggestion is [`Suggest::FileCompletion`], it prints `"_file_"` and + /// exits, instructing the shell to fall back to file completion. + /// - If the suggestion is [`Suggest::Suggest`] with a set of candidates, it + /// formats and prints them according to the shell type (Zsh/PowerShell, Fish, + /// or default). pub fn render_suggest<P>(ctx: ShellContext, suggest: Suggest) where P: ProgramCollect<Enum = P> + Display + 'static, diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 4996b19..2f63cf6 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -1,3 +1,4 @@ +#![deny(missing_docs)] //! Mingling Core //! //! # Intro @@ -8,27 +9,75 @@ //! //! Recommended to import [mingling](https://crates.io/crates/mingling) to use its features. +// Private Modules + mod any; mod asset; mod program; mod renderer; mod tester; +/// Module for setting up a `Mingling` program. +/// +/// This module provides the [`ProgramSetup`] type, which allows users to configure +/// and initialize the program's execution environment. +pub mod setup { + pub use crate::program::setup::ProgramSetup; +} + +/// This module provides result types for Mingling components. +/// +/// These are re-exported at the top level via `mingling::res`. +#[doc(hidden)] +pub mod core_res { + #[cfg(feature = "repl")] + pub use crate::program::repl_exec::res::*; +} + +/// Provides the runtime logic for Mingling's dynamic completion system. +/// +/// This module contains the core functionality for the "comp" (completion) feature, +/// which enables dynamic tab-completion and input suggestion capabilities within +/// Mingling applications. +#[cfg(feature = "comp")] +pub(crate) mod comp; + +/// Provides Mingling's build script module, used in `build.rs` to provide build-time behavior for certain features. +/// +/// To use it, add the following to your `Cargo.toml` under `[build-dependencies]`, and enable the features +/// that require build-time behavior from the crate: +/// +/// ```toml +/// [build-dependencies.mingling] +/// version = "0.3.0" +/// features = [ +/// "build", // Enable it +/// "comp", // If you need completion-related build-time behavior, enable this as well +/// ] +/// ``` +#[cfg(feature = "build")] +#[doc(hidden)] +pub mod build; + +// Public Modules + /// Provides a toolkit for `Mingling` testing capabilities. pub mod test { pub use crate::tester::*; } -#[cfg(feature = "structural_renderer")] -pub use crate::renderer::structural::StructuralRenderer; +/// Provided for framework developers +pub mod debug; // NOT re-exported at top level: the `StructuralData` trait is sealed and only // accessible through the derive macro. Users who need the trait can access it // via `mingling::renderer::structural::StructuralData` (through the inner alias). -pub use crate::any::group::*; -pub use crate::any::*; +#[cfg(feature = "structural_renderer")] +pub use crate::renderer::structural::StructuralRenderer; +pub use crate::any::*; pub use crate::asset::chain::*; +pub use crate::asset::core_invokes::*; pub use crate::asset::dispatcher::*; pub use crate::asset::enum_tag::*; pub use crate::asset::global_resource::*; @@ -37,64 +86,32 @@ pub use crate::asset::lazy_resource::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; pub use crate::asset::routable::*; +#[cfg(feature = "comp")] +pub use crate::comp::*; +pub use crate::program::*; +pub use crate::renderer::render_result::*; /// All error types of `Mingling` pub mod error { pub use crate::asset::chain::error::*; pub use crate::exec::error::*; pub use crate::program::error::*; + #[cfg(feature = "structural_renderer")] pub use crate::renderer::structural::error::*; - #[cfg(feature = "pathf")] - pub use mingling_pathf::error::*; -} - -pub use crate::program::*; - -pub use crate::renderer::render_result::*; - -#[cfg(feature = "builds")] -#[doc(hidden)] -pub mod builds; - -/// Provides build scripts for users -#[cfg(feature = "builds")] -pub mod build { - #[cfg(feature = "comp")] - pub use crate::builds::comp::*; #[cfg(feature = "pathf")] - pub use crate::builds::pathf::*; + pub use mingling_pathf::error::*; } -/// Provided for framework developers -pub mod debug; - -#[cfg(feature = "comp")] #[doc(hidden)] -pub mod comp; +mod private; -#[cfg(feature = "comp")] -pub use crate::comp::*; - -pub mod setup { - pub use crate::program::setup::ProgramSetup; -} - -/// Private API — not intended for direct use. +/// Internal API provided by Mingling Core +/// +/// These are used by macros and are not exposed to users, but are still accessible externally. #[doc(hidden)] +#[allow(unused_imports)] pub mod __private { - /// Sealed trait for `StructuralData` — only implementable via derive macro. - pub trait StructuralDataSealed {} - - /// Re-export so the derive macro can reference the trait without - /// conflicting with the derive macro name at `::mingling::StructuralData`. - #[cfg(feature = "structural_renderer")] - pub use crate::renderer::structural::structural_data::StructuralData; -} - -#[doc(hidden)] -pub mod core_res { - #[cfg(feature = "repl")] - pub use crate::program::repl_exec::res::ResREPL; + pub use crate::private::*; } diff --git a/mingling_core/src/private.rs b/mingling_core/src/private.rs new file mode 100644 index 0000000..371ada2 --- /dev/null +++ b/mingling_core/src/private.rs @@ -0,0 +1,12 @@ +/// Sealed trait for `StructuralData` — only implementable via derive macro. +#[cfg(feature = "structural_renderer")] +pub trait StructuralDataSealed<C> +where + C: crate::ProgramCollect<Enum = C>, +{ +} + +/// Re-export so the derive macro can reference the trait without +/// conflicting with the derive macro name at `::mingling::StructuralData`. +#[cfg(feature = "structural_renderer")] +pub use crate::renderer::structural::structural_data::StructuralData; diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs index 0d409b7..11e1bbf 100644 --- a/mingling_core/src/program.rs +++ b/mingling_core/src/program.rs @@ -53,10 +53,30 @@ where #[cfg(not(feature = "dispatch_tree"))] pub(crate) dispatcher: Vec<Box<dyn Dispatcher<C> + Send + Sync>>, + /// Program stdout settings. + /// + /// This struct controls the program's output behavior, including whether + /// to output errors, render results, suppress panic messages, enable + /// verbose/quiet/debug modes, colored output, and progress indicators. + /// + /// # Convention-only fields + /// + /// Fields marked with convention only are not enforced by the framework; + /// they serve as a shared convention for applications to follow consistently. pub stdout_setting: ProgramStdoutSetting, + + /// User-defined context that can be accessed throughout the program. + /// + /// This field holds a user-defined context value that can be + /// accessed and modified at any point during program execution. It is + /// useful for passing shared state or configuration across different + /// parts of the program. pub user_context: ProgramUserContext, #[cfg(feature = "structural_renderer")] + /// Setting for the structural renderer. + /// + /// This field is only available when the `structural_renderer` feature is enabled. pub structural_renderer_name: StructuralRendererSetting, pub(crate) hooks: Vec<ProgramHook<C>>, diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs index fe78979..14705ac 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -21,8 +21,16 @@ pub use mock::*; pub trait ProgramCollect { /// Enum type representing internal IDs for the program type Enum; + /// Error type when a dispatcher is not found for the given member type ErrorDispatcherNotFound: Grouped<Self::Enum>; + + /// Error type when a renderer is not found for the given member type ErrorRendererNotFound: Grouped<Self::Enum>; + + /// Result type for an empty chain result + /// + /// When the `extra_macros` feature is enabled, + /// you can use the `empty_result!()` macro to create this type ResultEmpty: Grouped<Self::Enum>; /// Use a prefix tree to quickly match arguments and dispatch to an Entry 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/exec.rs b/mingling_core/src/program/exec.rs index f5bdc2b..f0322a5 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -8,184 +8,15 @@ use crate::{ #[doc(hidden)] pub mod error; -#[cfg(feature = "async")] -pub async fn exec<C>( - program: &'static Program<C>, -) -> Result<RenderResult, ProgramInternalExecuteError> -where - C: ProgramCollect<Enum = C>, -{ - exec_with_args(program, &program.args).await -} - -#[cfg(not(feature = "async"))] +#[might_be_async::func] pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInternalExecuteError> where C: ProgramCollect<Enum = C>, { - exec_with_args(program, &program.args) -} - -#[cfg(feature = "async")] -pub async fn exec_with_args<C>( - program: &'static Program<C>, - args: &[String], -) -> Result<RenderResult, ProgramInternalExecuteError> -where - C: ProgramCollect<Enum = C>, -{ - // Exit code - let mut exit_code: i32 = 0; - - macro_rules! control { - ($hook_call:expr, $current:expr) => { - let __ccc = $hook_call; - if let Some(r) = - handle_program_control(program, __ccc, Some(&mut $current), &mut exit_code) - { - return Ok(r); - } - }; - ($hook_call:expr) => { - let __ccc = $hook_call; - if let Some(r) = handle_program_control(program, __ccc, None, &mut exit_code) { - return Ok(r); - } - }; - } - - // Current - let mut current = C::build_dispatcher_not_found(vec![]); - - // Run hooks - control!( - program.run_hook_pre_dispatch(crate::hook::HookPreDispatchInfo { arguments: args }), - current - ); - - // Dispatch args - either via dynamic dispatch or trie dispatch based on feature flag - let mut current = if cfg!(not(feature = "dispatch_tree")) { - dispatch_args_dynamic(program, args)? - } else { - C::dispatch_args_trie(args)? - }; - - // Run hook - control!( - program.run_hook_post_dispatch(crate::hook::HookPostDispatchInfo { - entry: ¤t.member_id, - }), - current - ); - - let mut stop_next = false; - - // If the program has Help enabled, skip actual logic and jump to Help - if program.user_context.help { - let mut render_result = render_help::<C>(program, current); - - // Run hook - control!(program.run_hook_finish(crate::hook::HookFinishInfo {})); - render_result.exit_code = exit_code; - - return Ok(render_result); - } - - loop { - let final_exec = stop_next; - - current = { - // If a chain exists, execute as a chain - if C::has_chain(¤t) { - // Run hook - control!( - program.run_hook_pre_chain(crate::hook::HookPreChainInfo { - input: ¤t.member_id, - raw: current.inner.as_ref(), - }), - current - ); - - match C::do_chain(current).await { - ChainProcess::Ok((any, NextProcess::Renderer)) => { - { - let mut render_result = render::<C>(program, any); - - // Run hook - control!(program.run_hook_finish(crate::hook::HookFinishInfo {})); - render_result.exit_code = exit_code; - - return Ok(render_result); - }; - } - ChainProcess::Ok((mut any, NextProcess::Chain)) => { - // Run hook - control!( - program.run_hook_post_chain(crate::hook::HookPostChainInfo { - output: &any - }), - any - ); - any - } - ChainProcess::Err(e) => { - // Run hook - control!( - program.run_hook_finish(crate::hook::HookFinishInfo {}), - &mut C::build_empty_result() - ); - return Err(e.into()); - } - } - } - // If no chain exists, attempt to render - else if C::has_renderer(¤t) { - // Run hook - control!( - program.run_hook_pre_render(crate::hook::HookPreRenderInfo { - input: ¤t.member_id, - raw: current.inner.as_ref(), - }), - current - ); - - let mut render_result = render::<C>(program, current); - - // Run hooks - control!( - program.run_hook_post_render(crate::hook::HookPostRenderInfo { - result: &render_result, - }) - ); - - control!(program.run_hook_finish(crate::hook::HookFinishInfo {})); - - render_result.exit_code = exit_code; - return Ok(render_result); - } - // No renderer exists - else { - stop_next = true; - C::build_renderer_not_found(current.member_id) - } - }; - - if final_exec && stop_next { - break; - } - } - let mut render_result = RenderResult::default(); - - // Run hook - control!( - program.run_hook_finish(crate::hook::HookFinishInfo {}), - current - ); - render_result.exit_code = exit_code; - Ok(render_result) + might_be_async::invoke!(exec_with_args(program, &program.args)) } -#[cfg(not(feature = "async"))] +#[might_be_async::func] pub fn exec_with_args<C>( program: &'static Program<C>, args: &[String], @@ -265,7 +96,7 @@ where current ); - match C::do_chain(current) { + match might_be_async::invoke!(C::do_chain(current)) { ChainProcess::Ok((any, NextProcess::Renderer)) => { { let mut render_result = render::<C>(program, any); 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..96723fd 100644 --- a/mingling_core/src/program/once_exec.rs +++ b/mingling_core/src/program/once_exec.rs @@ -1,28 +1,10 @@ use crate::THIS_PROGRAM; use crate::{Program, ProgramCollect, RenderResult, error::ProgramExecuteError}; -// Async program -#[cfg(feature = "async")] impl<C> Program<C> where C: ProgramCollect<Enum = C>, { - pub(crate) async fn exec_wrapper<F, Fut>(self, f: F) -> Fut::Output - where - C: 'static + Send + Sync, - F: FnOnce(&'static Program<C>) -> Fut + Send + Sync, - Fut: Future + Send, - { - THIS_PROGRAM.set(Box::new(self)); - let program = THIS_PROGRAM - .get_raw() - .unwrap() - .downcast_ref::<Program<C>>() - .unwrap(); - - f(program).await - } - /// Run the command line program /// /// # Errors @@ -33,7 +15,8 @@ where /// # Panics /// /// Panics if the program encounters a non-recoverable internal error. - pub async fn exec_without_render(mut self) -> Result<RenderResult, ProgramExecuteError> + #[might_be_async::func] + pub fn exec_without_render(mut self) -> Result<RenderResult, ProgramExecuteError> where C: 'static + Send + Sync, { @@ -42,21 +25,56 @@ where self.args = self.args.iter().skip(1).cloned().collect(); - return self - .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(|e| e.into()) }) - .await; + #[cfg(not(feature = "async"))] + { + #[cfg(panic = "abort")] + return self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into())); + + #[cfg(not(panic = "abort"))] + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.exec_wrapper(|p| crate::exec::exec(p).map_err(std::convert::Into::into)) + })) { + Ok(result) => result, + Err(panic_info) => { + let panic_payload = crate::error::ProgramPanic { + payload: panic_info, + }; + + let program = THIS_PROGRAM + .get_raw() + .unwrap() + .downcast_ref::<Program<C>>() + .unwrap(); + + #[cfg(not(feature = "async"))] + program.run_hook_exec_panic(crate::hook::HookPanicInfo { + panic: &panic_payload, + }); + + Err(ProgramExecuteError::Panic(panic_payload)) + } + } + } + + #[cfg(feature = "async")] + { + return self + .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(|e| e.into()) }) + .await; + } } /// Run the command line program #[must_use] - pub async fn exec(self) -> i32 + #[might_be_async::func] + pub fn exec(self) -> i32 where C: 'static + Send + Sync, { use crate::error::ProgramExecuteError; let stdout_setting = self.stdout_setting.clone(); - let result = match self.exec_without_render().await { + let result = match might_be_async::invoke!(self.exec_without_render()) { Ok(r) => r, Err(e) => match e { ProgramExecuteError::DispatcherNotFound => { @@ -79,31 +97,21 @@ 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 - pub async fn exec_and_exit(self) + #[might_be_async::func] + pub fn exec_and_exit(self) where C: 'static + Send + Sync, { - let exit_code = self.exec().await; + let exit_code = might_be_async::invoke!(self.exec()); // SAFETY: exec() is synchronous — it returns only after all // chain handlers and renderers have finished. No code still // holds references from get_raw() at this point. @@ -112,6 +120,29 @@ where } } +// Async program +#[cfg(feature = "async")] +impl<C> Program<C> +where + C: ProgramCollect<Enum = C>, +{ + pub(crate) async fn exec_wrapper<F, Fut>(self, f: F) -> Fut::Output + where + C: 'static + Send + Sync, + F: FnOnce(&'static Program<C>) -> Fut + Send + Sync, + Fut: Future + Send, + { + THIS_PROGRAM.set(Box::new(self)); + let program = THIS_PROGRAM + .get_raw() + .unwrap() + .downcast_ref::<Program<C>>() + .unwrap(); + + f(program).await + } +} + // Sync program #[cfg(not(feature = "async"))] impl<C> Program<C> @@ -137,115 +168,4 @@ where f(program) } - - /// Run the command line program - /// - /// # Errors - /// - /// Returns `Err(ProgramExecuteError)` if execution fails, - /// e.g., if no dispatcher is found or a chain error occurs. - /// - /// # Panics - /// - /// Panics if the program encounters a non-recoverable internal error. - pub fn exec_without_render(mut self) -> Result<RenderResult, ProgramExecuteError> - where - C: 'static + Send + Sync, - { - // Run hooks - self.run_hook_on_begin(crate::hook::HookBeginInfo {}); - - self.args = self.args.iter().skip(1).cloned().collect(); - - #[cfg(panic = "abort")] - return self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into())); - - #[cfg(not(panic = "abort"))] - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - self.exec_wrapper(|p| crate::exec::exec(p).map_err(std::convert::Into::into)) - })) { - Ok(result) => result, - Err(panic_info) => { - let panic_payload = crate::error::ProgramPanic { - payload: panic_info, - }; - - let program = THIS_PROGRAM - .get_raw() - .unwrap() - .downcast_ref::<Program<C>>() - .unwrap(); - - program.run_hook_exec_panic(crate::hook::HookPanicInfo { - panic: &panic_payload, - }); - - Err(ProgramExecuteError::Panic(panic_payload)) - } - } - } - - /// Run the command line program - #[must_use] - pub fn exec(self) -> i32 - where - C: 'static + Send + Sync, - { - use crate::error::ProgramExecuteError; - - let stdout_setting = self.stdout_setting.clone(); - let result = match self.exec_without_render() { - Ok(r) => r, - Err(e) => match e { - ProgramExecuteError::DispatcherNotFound => { - eprintln!("Dispatcher not found"); - return 1; - } - ProgramExecuteError::RendererNotFound(renderer_name) => { - eprintln!("Renderer `{renderer_name}` not found"); - return 1; - } - ProgramExecuteError::Other(e) => { - eprintln!("{e}"); - return 1; - } - ProgramExecuteError::Panic(unwinded_error) => { - eprintln!("{unwinded_error}"); - return 1; - } - }, - }; - - // 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 - } - } - - /// Run the command line program, then exit - pub fn exec_and_exit(self) - where - C: 'static + Send + Sync, - { - let exit_code = self.exec(); - // SAFETY: exec() is synchronous — it returns only after all - // chain handlers and renderers have finished. No code still - // holds references from get_raw() at this point. - drop(unsafe { THIS_PROGRAM.take() }); - std::process::exit(exit_code) - } } diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs index cbda9da..f84b291 100644 --- a/mingling_core/src/program/repl_exec.rs +++ b/mingling_core/src/program/repl_exec.rs @@ -13,7 +13,6 @@ use crate::program::repl_exec::splitter::split_input_string; use crate::{Program, ProgramCollect, RenderResult}; use crate::{program::repl_exec::res::ResREPL, this}; -#[cfg(not(feature = "async"))] impl<C> Program<C> where C: ProgramCollect<Enum = C> + Send + Sync + 'static, @@ -22,106 +21,63 @@ where /// /// This method starts an infinite loop that continuously reads user input, parses commands, executes them, /// and displays the execution result or error message. It is suitable for scenarios requiring command-line interaction with the user. + /// + /// **Note:** When the `async` feature is enabled, panic unwinding is not supported. + /// Any panics during command execution will result in an abort rather than being caught and handled gracefully. + #[might_be_async::func] pub fn exec_repl(mut self) { // Inject default REPL resource self.with_resource(ResREPL::default()); self.run_hook_repl_on_begin(crate::hook::HookREPLBeginInfo {}); - self.exec_wrapper(|p| -> () { - loop { - p.run_hook_repl_pre_readline(crate::hook::HookREPLPreReadlineInfo {}); - let mut readline = p - .run_hook_repl_readline(crate::hook::HookREPLReadlineInfo {}) - .unwrap_or_default(); - p.run_hook_repl_post_readline(crate::hook::HookREPLPostReadlineInfo { - line: &mut readline, - }); - - let args = split_input_string(readline.clone()); - - p.run_hook_repl_pre_exec(crate::hook::HookREPLPreExecInfo { args: &args }); - match exec_once(p, args) { - Ok(r) => { - p.run_hook_repl_on_receive_result( - crate::hook::HookREPLOnReceiveResultInfo { result: &r }, - ); - } - Err(ProgramInternalExecuteError::REPLPanic(panic)) => { - p.run_hook_repl_on_panic(crate::hook::HookREPLOnPanicInfo { - panic: &panic, - }); - } - _ => {} - } - p.run_hook_repl_post_exec(crate::hook::HookREPLPostExecInfo {}); - - if this::<C>().res::<ResREPL>().unwrap().exit { - p.run_hook_repl_exit(crate::hook::HookREPLExitInfo {}); - break; - } - - p.run_hook_repl_loop_once(crate::hook::HookREPLLoopOnceInfo {}); - } - }); + might_be_async::select!( + self.exec_wrapper(async |p| -> () { + repl_loop(p).await; + }) + else + self.exec_wrapper(|p| -> () { repl_loop(p); } + ) + ); } } -#[cfg(feature = "async")] -impl<C> Program<C> +#[might_be_async::func] +fn repl_loop<C>(p: &'static Program<C>) where C: ProgramCollect<Enum = C> + Send + Sync + 'static, { - /// Executes the REPL interactive CLI mode. - /// - /// This method starts an infinite loop that continuously reads user input, parses commands, executes them, - /// and displays the execution result or error message. It is suitable for scenarios requiring command-line interaction with the user. - /// - /// **Note:** When the `async` feature is enabled, panic unwinding is not supported. - /// Any panics during command execution will result in an abort rather than being caught and handled gracefully. - pub async fn exec_repl(mut self) { - // Inject default REPL resource - self.with_resource(ResREPL::default()); + loop { + p.run_hook_repl_pre_readline(crate::hook::HookREPLPreReadlineInfo {}); + let mut readline = p + .run_hook_repl_readline(crate::hook::HookREPLReadlineInfo {}) + .unwrap_or_default(); + p.run_hook_repl_post_readline(crate::hook::HookREPLPostReadlineInfo { + line: &mut readline, + }); - self.run_hook_repl_on_begin(crate::hook::HookREPLBeginInfo {}); + let args = split_input_string(readline.clone()); - self.exec_wrapper(async |p| -> () { - loop { - p.run_hook_repl_pre_readline(crate::hook::HookREPLPreReadlineInfo {}); - let mut readline = p - .run_hook_repl_readline(crate::hook::HookREPLReadlineInfo {}) - .unwrap_or_default(); - p.run_hook_repl_post_readline(crate::hook::HookREPLPostReadlineInfo { - line: &mut readline, + p.run_hook_repl_pre_exec(crate::hook::HookREPLPreExecInfo { args: &args }); + match might_be_async::invoke!(exec_once(p, args)) { + Ok(r) => { + p.run_hook_repl_on_receive_result(crate::hook::HookREPLOnReceiveResultInfo { + result: &r, }); - - let args = split_input_string(readline.clone()); - - p.run_hook_repl_pre_exec(crate::hook::HookREPLPreExecInfo { args: &args }); - match exec_once(p, args).await { - Ok(r) => { - p.run_hook_repl_on_receive_result( - crate::hook::HookREPLOnReceiveResultInfo { result: &r }, - ); - } - Err(ProgramInternalExecuteError::REPLPanic(panic)) => { - p.run_hook_repl_on_panic(crate::hook::HookREPLOnPanicInfo { - panic: &panic, - }); - } - _ => {} - } - p.run_hook_repl_post_exec(crate::hook::HookREPLPostExecInfo {}); - - if this::<C>().res::<ResREPL>().unwrap().exit { - p.run_hook_repl_exit(crate::hook::HookREPLExitInfo {}); - break; - } - - p.run_hook_repl_loop_once(crate::hook::HookREPLLoopOnceInfo {}); } - }) - .await; + Err(ProgramInternalExecuteError::REPLPanic(panic)) => { + p.run_hook_repl_on_panic(crate::hook::HookREPLOnPanicInfo { panic: &panic }); + } + _ => {} + } + p.run_hook_repl_post_exec(crate::hook::HookREPLPostExecInfo {}); + + if this::<C>().res::<ResREPL>().unwrap().exit { + p.run_hook_repl_exit(crate::hook::HookREPLExitInfo {}); + break; + } + + p.run_hook_repl_loop_once(crate::hook::HookREPLLoopOnceInfo {}); } } 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 e57a5b9..8757376 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -1,22 +1,67 @@ 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)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] 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. + /// + /// A value of `0` indicates success, while non-zero values indicate + /// various error conditions. 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()) } @@ -27,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()) } } @@ -64,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) } } @@ -119,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: &str) { - self.render_text.push_str(text); + 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. @@ -142,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 println(&mut self, text: &str) { - self.render_text.push_str(text); - self.render_text.push('\n'); + 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 + /// + /// ``` + /// 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. @@ -169,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() } } @@ -186,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"); @@ -217,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(); @@ -236,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] @@ -265,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/error.rs b/mingling_core/src/renderer/structural/error.rs index a7fbc75..63ded81 100644 --- a/mingling_core/src/renderer/structural/error.rs +++ b/mingling_core/src/renderer/structural/error.rs @@ -9,6 +9,7 @@ pub struct StructuralRendererSerializeError { } impl StructuralRendererSerializeError { + /// Creates a new `StructuralRendererSerializeError` with the given error message. #[must_use] pub fn new(error: String) -> Self { Self { error } 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>, +{ +} |
