aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/any.rs143
-rw-r--r--mingling_core/src/any/group.rs36
-rw-r--r--mingling_core/src/asset.rs36
-rw-r--r--mingling_core/src/asset/chain.rs1
-rw-r--r--mingling_core/src/asset/core_invokes.rs271
-rw-r--r--mingling_core/src/asset/dispatcher.rs40
-rw-r--r--mingling_core/src/asset/global_resource.rs65
-rw-r--r--mingling_core/src/asset/lazy_resource.rs12
-rw-r--r--mingling_core/src/build.rs13
-rw-r--r--mingling_core/src/build/comp.rs (renamed from mingling_core/src/builds/comp.rs)0
-rw-r--r--mingling_core/src/build/pathf.rs (renamed from mingling_core/src/builds/pathf.rs)0
-rw-r--r--mingling_core/src/builds.rs7
-rw-r--r--mingling_core/src/comp.rs55
-rw-r--r--mingling_core/src/lib.rs113
-rw-r--r--mingling_core/src/private.rs12
-rw-r--r--mingling_core/src/program.rs20
-rw-r--r--mingling_core/src/program/collection.rs8
-rw-r--r--mingling_core/src/program/collection/mock.rs7
-rw-r--r--mingling_core/src/program/exec.rs177
-rw-r--r--mingling_core/src/program/hook.rs15
-rw-r--r--mingling_core/src/program/once_exec.rs224
-rw-r--r--mingling_core/src/program/repl_exec.rs126
-rw-r--r--mingling_core/src/program/setup.rs3
-rw-r--r--mingling_core/src/renderer/render_result.rs504
-rw-r--r--mingling_core/src/renderer/structural.rs16
-rw-r--r--mingling_core/src/renderer/structural/error.rs1
-rw-r--r--mingling_core/src/renderer/structural/structural_data.rs8
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: &current.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(&current) {
- // Run hook
- control!(
- program.run_hook_pre_chain(crate::hook::HookPreChainInfo {
- input: &current.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(&current) {
- // Run hook
- control!(
- program.run_hook_pre_render(crate::hook::HookPreRenderInfo {
- input: &current.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>,
+{
+}