aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/lib.rs3
-rw-r--r--mingling_core/src/program/exec.rs9
-rw-r--r--mingling_core/src/program/hook.rs31
-rw-r--r--mingling_core/src/program/hook/hook_info.rs5
-rw-r--r--mingling_core/src/program/once_exec.rs14
-rw-r--r--mingling_core/src/program/repl_exec.rs14
-rw-r--r--mingling_core/src/renderer/render_result.rs333
-rw-r--r--mingling_core/src/utils.rs3
-rw-r--r--mingling_core/src/utils/splitter.rs (renamed from mingling_core/src/program/repl_exec/splitter.rs)78
9 files changed, 399 insertions, 91 deletions
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 7048307..641118c 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -110,3 +110,6 @@ pub mod __private {
/// Mingling's convention metadatas, which can be bound to types using `#[metadata]`, to provide identification for types
pub mod metadata;
+
+/// Some common utilities in Mingling, providing a collection of functionality needed by many modules.
+pub mod utils;
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index 923b47b..b0a5b16 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -15,13 +15,14 @@ pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInte
where
C: ProgramCollect<Enum = C> + Send + Sync,
{
- might_be_async::invoke!(exec_with_args(program, &program.args))
+ let mut args = program.args.clone();
+ might_be_async::invoke!(exec_with_args(program, &mut args))
}
#[might_be_async::func]
pub fn exec_with_args<C>(
program: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync,
@@ -51,7 +52,9 @@ where
// Run hooks
control!(
- program.run_hook_pre_dispatch(&crate::hook::HookPreDispatchInfo { arguments: args }),
+ program.run_hook_pre_dispatch(&mut crate::hook::HookPreDispatchInfo {
+ arguments: &mut *args,
+ }),
current
);
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 8fd2ba1..92106f9 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -64,8 +64,9 @@ where
pub begin: Option<Box<dyn Fn(&HookBeginInfo) + Send + Sync>>,
/// Executes before the program dispatches
- pub pre_dispatch:
- Option<Box<dyn for<'a> Fn(&HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>>,
+ pub pre_dispatch: Option<
+ Box<dyn for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>,
+ >,
/// Executes after the program dispatches
pub post_dispatch: Option<
@@ -162,7 +163,10 @@ where
}
}
- pub(crate) fn run_hook_pre_dispatch(&self, info: &HookPreDispatchInfo) -> ProgramControls<C> {
+ pub(crate) fn run_hook_pre_dispatch(
+ &self,
+ info: &mut HookPreDispatchInfo,
+ ) -> ProgramControls<C> {
if !self.user_context.run_hook {
return ProgramControls::Empty;
}
@@ -475,7 +479,7 @@ where
#[must_use]
pub fn on_pre_dispatch<F, R>(mut self, handler: F) -> Self
where
- F: for<'a> Fn(&HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
+ F: for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
R: Into<ProgramControls<C>>,
{
self.pre_dispatch = Some(Box::new(move |info| handler(info).into()));
@@ -801,16 +805,21 @@ mod tests {
#[test]
fn test_hook_on_pre_dispatch() {
static CALLED: AtomicBool = AtomicBool::new(false);
- let hook =
- ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(|info: &HookPreDispatchInfo| {
- assert_eq!(info.arguments, &["a", "b"]);
+ let mut args = vec!["a".to_string(), "b".to_string()];
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(
+ |info: &mut HookPreDispatchInfo| {
+ assert_eq!(info.arguments.as_slice(), &["a", "b"]);
+ // The hook may rewrite the arguments before dispatch
+ info.arguments.push("c".to_string());
CALLED.store(true, Ordering::SeqCst);
- });
+ },
+ );
assert!(hook.pre_dispatch.is_some());
- (hook.pre_dispatch.as_ref().unwrap())(&HookPreDispatchInfo {
- arguments: &["a".to_string(), "b".to_string()],
+ (hook.pre_dispatch.as_ref().unwrap())(&mut HookPreDispatchInfo {
+ arguments: &mut args,
});
assert!(CALLED.load(Ordering::SeqCst));
+ assert_eq!(args.as_slice(), &["a", "b", "c"]);
}
#[test]
@@ -910,7 +919,7 @@ mod tests {
fn test_hook_builder_chaining() {
let hook = ProgramHook::<MockHookEnum>::empty()
.on_begin::<_, ()>(|_: &HookBeginInfo| ())
- .on_pre_dispatch(|_: &HookPreDispatchInfo| ())
+ .on_pre_dispatch(|_: &mut HookPreDispatchInfo| ())
.on_post_dispatch(|_: &HookPostDispatchInfo<MockHookEnum>| ())
.on_pre_chain(|_: &HookPreChainInfo<MockHookEnum>| ())
.on_post_chain(|_: &HookPostChainInfo<MockHookEnum>| ())
diff --git a/mingling_core/src/program/hook/hook_info.rs b/mingling_core/src/program/hook/hook_info.rs
index 768f652..25cf272 100644
--- a/mingling_core/src/program/hook/hook_info.rs
+++ b/mingling_core/src/program/hook/hook_info.rs
@@ -7,7 +7,10 @@ pub struct HookBeginInfo {}
/// Represents the data passed to `pre_dispatch` hook.
pub struct HookPreDispatchInfo<'a> {
/// Arguments entered by the user before dispatching
- pub arguments: &'a [String],
+ ///
+ /// The reference is mutable so the hook can rewrite the arguments before
+ /// they are matched against the registered dispatchers.
+ pub arguments: &'a mut Vec<String>,
}
/// Represents the data passed to `post_dispatch` hook.
diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs
index e9927b5..203bbf5 100644
--- a/mingling_core/src/program/once_exec.rs
+++ b/mingling_core/src/program/once_exec.rs
@@ -25,15 +25,19 @@ where
self.run_hook_on_begin(&crate::hook::HookBeginInfo {});
self.args = self.args.iter().skip(1).cloned().collect();
+ let mut args = std::mem::take(&mut self.args);
#[cfg(not(feature = "async"))]
{
#[cfg(panic = "abort")]
- return self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into()));
+ return self
+ .exec_wrapper(|p| crate::exec::exec_with_args(p, &mut args).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))
+ self.exec_wrapper(|p| {
+ crate::exec::exec_with_args(p, &mut args).map_err(std::convert::Into::into)
+ })
})) {
Ok(result) => result,
Err(panic_info) => {
@@ -60,7 +64,11 @@ where
#[cfg(feature = "async")]
{
return self
- .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(Into::into) })
+ .exec_wrapper(|p| async move {
+ crate::exec::exec_with_args(p, &mut args)
+ .await
+ .map_err(Into::into)
+ })
.await;
}
}
diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs
index 9d9be30..2db5c5a 100644
--- a/mingling_core/src/program/repl_exec.rs
+++ b/mingling_core/src/program/repl_exec.rs
@@ -7,10 +7,8 @@ use std::io::Write;
#[doc(hidden)]
pub mod res;
-mod splitter;
-
use crate::error::{ProgramInternalExecuteError, ProgramPanic};
-use crate::program::repl_exec::splitter::split_input_string;
+use crate::utils::ArgumentSplitter;
use crate::{Program, ProgramCollect, RenderResult};
use crate::{program::repl_exec::res::ResREPL, this};
@@ -58,10 +56,10 @@ where
line: &mut readline,
});
- let args = split_input_string(&readline);
+ let mut args = readline.split_args();
p.run_hook_repl_pre_exec(&crate::hook::HookREPLPreExecInfo { args: &args });
- match might_be_async::invoke!(exec_once(p, &args)) {
+ match might_be_async::invoke!(exec_once(p, &mut args)) {
Ok(r) => {
p.run_hook_repl_on_receive_result(&crate::hook::HookREPLOnReceiveResultInfo {
result: &r,
@@ -91,13 +89,13 @@ where
#[cfg(not(feature = "async"))]
fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,
{
#[cfg(panic = "abort")]
- let exec_result = super::exec::exec_with_args(p, &args);
+ let exec_result = super::exec::exec_with_args(p, args);
#[cfg(not(panic = "abort"))]
let exec_result = {
@@ -130,7 +128,7 @@ where
#[cfg(feature = "async")]
async fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 0351925..22d787c 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,11 +1,99 @@
use std::{
- fmt::{Display, Formatter},
+ fmt::{self, Display, Formatter},
io::Write,
process::{ExitCode, exit},
};
use crate::RenderResultMode::{Stderr, Stdout};
+/// A single emitted output item handed to a print hook.
+///
+/// `RenderResultPrint` bundles the text content and the output mode together
+/// into one value, so a print hook can route the content to stdout/stderr — or
+/// any custom sink — as a unit instead of juggling two separate arguments.
+///
+/// Values of this type are produced whenever a [`RenderResult`] with bound
+/// print hooks (see [`RenderResult::bind_print_hook`] and
+/// [`RenderResult::immediate_output`]) writes content through
+/// `print`/`println`/`eprint`/`eprintln`, and are handed to every hook in
+/// binding order. They are also used to flush another result's buffered content
+/// via [`RenderResult::append_other`] when the destination has hooks bound.
+///
+/// # Fields
+///
+/// * `content` — The raw text that was written, including any trailing newline
+/// added by `println`/`eprintln`.
+/// * `mode` — The output mode (`Stdout` or `Stderr`) the content was written
+/// with, which tells the hook where the content belongs.
+///
+/// # Examples
+///
+/// ```
+/// use mingling_core::{RenderResult, RenderResultMode, RenderResultPrint};
+///
+/// // Build an output item manually
+/// let print = RenderResultPrint {
+/// content: "Hello, world!".to_string(),
+/// mode: RenderResultMode::Stdout,
+/// };
+/// assert_eq!(print.content, "Hello, world!");
+/// assert_eq!(print.mode, RenderResultMode::Stdout);
+///
+/// // Use it inside a print hook
+/// let mut result = RenderResult::default();
+/// result.bind_print_hook(|print| match print.mode {
+/// RenderResultMode::Stdout => print!("{}", print.content),
+/// RenderResultMode::Stderr => eprint!("{}", print.content),
+/// });
+/// result.eprintln("something went wrong"); // goes to stderr via the hook
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RenderResultPrint {
+ /// The emitted text content.
+ ///
+ /// This is the raw text that was written to the render buffer when the
+ /// hook fired. For `println`/`eprintln` it includes the trailing newline;
+ /// for `print`/`eprint` it is exactly the given text.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResultMode, RenderResultPrint};
+ ///
+ /// let print = RenderResultPrint {
+ /// content: "Hello".to_string(),
+ /// mode: RenderResultMode::Stdout,
+ /// };
+ /// assert_eq!(print.content, "Hello");
+ /// ```
+ pub content: String,
+
+ /// The output mode the content was written with.
+ ///
+ /// Indicates whether the content was originally directed to stdout
+ /// (`Stdout`) or stderr (`Stderr`), allowing a hook to route the content
+ /// to the matching stream.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResultMode, RenderResultPrint};
+ ///
+ /// let print = RenderResultPrint {
+ /// content: "error".to_string(),
+ /// mode: RenderResultMode::Stderr,
+ /// };
+ /// assert_eq!(print.mode, RenderResultMode::Stderr);
+ /// ```
+ pub mode: RenderResultMode,
+}
+
+/// Optional list of print hooks bound to a `RenderResult`.
+///
+/// Each hook receives the emitted [`RenderResultPrint`]. See
+/// [`RenderResult::bind_print_hook`] and [`RenderResult::immediate_output`].
+type PrintHook = Option<Vec<Box<dyn FnMut(RenderResultPrint)>>>;
+
/// Render result, containing the rendered text content.
///
/// `RenderResult` is the core data structure used throughout the rendering pipeline
@@ -20,8 +108,9 @@ use crate::RenderResultMode::{Stderr, Stdout};
/// - **Buffered output**: All rendered content is first collected into the buffer
/// and can be output uniformly at a convenient time.
/// - **Immediate output**: Can be enabled via [`immediate_output`](RenderResult::immediate_output),
-/// causing content to be flushed to stdout/stderr in real time while also being
-/// added to the buffer.
+/// which binds a print hook that flushes content to stdout/stderr in real time
+/// while also being added to the buffer. Custom hooks can be bound with
+/// [`bind_print_hook`](RenderResult::bind_print_hook).
/// - **Dual-channel output**: The `Stdout` and `Stderr` modes distinguish between
/// normal output and error output.
/// - **Exit code management**: Supports carrying an exit code to exit the process
@@ -59,19 +148,19 @@ use crate::RenderResultMode::{Stderr, Stdout};
/// let result: RenderResult = (|| RenderResult::from("closure result")).into();
/// assert_eq!(result.to_string(), "closure result");
/// ```
-#[derive(Default, Debug, Clone, PartialEq, Eq)]
+#[derive(Default)]
pub struct RenderResult {
- /// Whether immediate output is enabled.
+ /// Print hooks invoked with the buffered content and its output mode.
///
- /// When set to `true`, rendered content is flushed to stdout/stderr in real time
- /// while also being written to the buffer, enabling live output. This is useful
- /// in scenarios where results should be displayed incrementally, such as in
- /// long-running rendering tasks where the user wants to see partial output
- /// without waiting for the entire rendering process to complete.
+ /// When hooks are bound (via [`immediate_output`](RenderResult::immediate_output)
+ /// or [`bind_print_hook`](RenderResult::bind_print_hook)), every
+ /// `print`/`println`/`eprint`/`eprintln` call additionally emits the content
+ /// through each hook in binding order — typically flushing it to stdout/stderr
+ /// in real time — while the content is still appended to the buffer.
///
- /// The default value is `false`, meaning all content is first written to the
- /// buffer and output uniformly at the end.
- immediate_output: bool,
+ /// The default value is `None`, meaning content is only buffered and output
+ /// uniformly at the end (e.g. via [`std_print`](RenderResult::std_print)).
+ print_hook: PrintHook,
/// Render buffer, stored as a list of (text, output mode) pairs.
///
@@ -111,6 +200,36 @@ pub struct RenderResult {
pub exit_code: i32,
}
+impl fmt::Debug for RenderResult {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.debug_struct("RenderResult")
+ .field("render_buffer", &self.render_buffer)
+ .field("exit_code", &self.exit_code)
+ .field("print_hooks", &self.print_hook.as_ref().map(Vec::len))
+ .finish()
+ }
+}
+
+impl Clone for RenderResult {
+ /// The bound print hooks are opaque closures and cannot be cloned, so the
+ /// cloned result is created without any hooks.
+ fn clone(&self) -> Self {
+ Self {
+ print_hook: None,
+ render_buffer: self.render_buffer.clone(),
+ exit_code: self.exit_code,
+ }
+ }
+}
+
+impl PartialEq for RenderResult {
+ fn eq(&self, other: &Self) -> bool {
+ self.render_buffer == other.render_buffer && self.exit_code == other.exit_code
+ }
+}
+
+impl Eq for RenderResult {}
+
/// Enum representing the output mode for render results.
///
/// This determines whether the rendered content should be directed to standard
@@ -244,12 +363,13 @@ impl RenderResult {
Self::default()
}
- /// Marks the render result for immediate output, bypassing any buffering or
- /// deferred rendering.
+ /// Enables immediate output by binding a print hook that flushes content to
+ /// stdout/stderr in real time.
///
- /// 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.
+ /// After this is called, every `print`/`println`/`eprint`/`eprintln` call
+ /// writes its content to the corresponding output stream immediately, while
+ /// also keeping it in the buffer for later use (e.g. [`std_print`](RenderResult::std_print)
+ /// or `to_string()`).
///
/// # Examples
///
@@ -258,9 +378,47 @@ impl RenderResult {
///
/// let mut result = RenderResult::default();
/// result.immediate_output();
+ /// result.print("Hello, ");
+ /// result.print("world!"); // flushed to stdout right away
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub const fn immediate_output(&mut self) -> &mut Self {
- self.immediate_output = true;
+ pub fn immediate_output(&mut self) -> &mut Self {
+ self.bind_print_hook(|RenderResultPrint { content, mode }| match mode {
+ Stdout => print!("{content}"),
+ Stderr => eprint!("{content}"),
+ })
+ }
+
+ /// Binds a custom print hook invoked with the content and output mode of
+ /// every `print`/`println`/`eprint`/`eprintln` call.
+ ///
+ /// Multiple hooks can be bound; they are invoked in binding order. This is
+ /// the building block behind [`immediate_output`](RenderResult::immediate_output)
+ /// and can be used to route output to a custom sink (e.g. for testing).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.bind_print_hook(|print| {
+ /// println!(
+ /// "[{}] {}",
+ /// if print.mode == RenderResultMode::Stdout {
+ /// "out"
+ /// } else {
+ /// "err"
+ /// },
+ /// print.content
+ /// );
+ /// });
+ /// result.print("Hello");
+ /// ```
+ pub fn bind_print_hook(&mut self, hook: impl FnMut(RenderResultPrint) + 'static) -> &mut Self {
+ self.print_hook
+ .get_or_insert_with(Vec::new)
+ .push(Box::new(hook));
self
}
@@ -317,12 +475,12 @@ impl RenderResult {
/// 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.
+ /// If this `RenderResult` has print hooks bound but the other does not, the
+ /// other's content is emitted through this result's hooks (e.g. flushed to
+ /// 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.
+ /// The `exit_code` and the print hooks of the other result are **not**
+ /// transferred — only its buffered content is merged.
///
/// # Arguments
///
@@ -345,17 +503,15 @@ impl RenderResult {
pub fn append_other(&mut self, other: impl Into<Self>) {
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;
+ // If self has hooks but the other does not, the other's buffered content
+ // was never emitted — flush it through self's hooks while appending.
+ let should_emit = self.print_hook.is_some() && other.print_hook.is_none();
- for i in other.render_buffer {
- if immediate_output {
- match &i.1 {
- Stdout => print!("{}", i.0),
- Stderr => eprint!("{}", i.0),
- }
+ for (content, mode) in other.render_buffer {
+ if should_emit {
+ self.emit(&content, mode);
}
- self.render_buffer.push(i);
+ self.render_buffer.push((content, mode));
}
}
@@ -373,9 +529,7 @@ impl RenderResult {
/// ```
pub fn print(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- print!("{text}");
- }
+ self.emit(&text, Stdout);
self.append_to_buffer(text, Stdout);
}
@@ -393,9 +547,7 @@ impl RenderResult {
/// ```
pub fn println(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- println!("{text}");
- }
+ self.emit(&format!("{text}\n"), Stdout);
self.append_line_to_buffer(text, Stdout);
}
@@ -413,9 +565,7 @@ impl RenderResult {
/// ```
pub fn eprint(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- eprint!("{text}");
- }
+ self.emit(&text, Stderr);
self.append_to_buffer(text, Stderr);
}
@@ -433,9 +583,7 @@ impl RenderResult {
/// ```
pub fn eprintln(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- eprintln!("{text}");
- }
+ self.emit(&format!("{text}\n"), Stderr);
self.append_line_to_buffer(text, Stderr);
}
@@ -538,7 +686,7 @@ impl RenderResult {
///
/// # Returns
///
- /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with
+ /// A new `RenderResult` with the same print hooks and `exit_code`, but with
/// trimmed text content.
///
/// # Examples
@@ -579,11 +727,23 @@ impl RenderResult {
Self {
render_buffer: buffer,
- immediate_output: self.immediate_output,
+ print_hook: self.print_hook,
exit_code: self.exit_code,
}
}
+ /// Emits `content` to every bound print hook, if any.
+ fn emit(&mut self, content: &str, mode: RenderResultMode) {
+ if let Some(hooks) = &mut self.print_hook {
+ for hook in hooks {
+ hook(RenderResultPrint {
+ content: content.to_string(),
+ mode,
+ });
+ }
+ }
+ }
+
/// Exits the process with the exit code stored in this `RenderResult`.
///
/// This method calls `std::process::exit()` with the `exit_code` value,
@@ -623,7 +783,9 @@ fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) ->
#[cfg(test)]
mod tests {
use super::*;
+ use std::cell::RefCell;
use std::io::Write as IoWrite;
+ use std::rc::Rc;
#[test]
fn default_creates_empty_text_with_exit_code_zero() {
@@ -749,4 +911,79 @@ mod tests {
assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr);
assert_eq!(trimmed.to_string(), "error");
}
+
+ #[test]
+ fn print_hooks_receive_content_and_mode() {
+ let mut result = RenderResult::default();
+ let captured: Rc<RefCell<Vec<RenderResultPrint>>> = Rc::default();
+ let hook_captured = Rc::clone(&captured);
+ result.bind_print_hook(move |print| hook_captured.borrow_mut().push(print));
+
+ result.print("Hello");
+ result.eprintln("World");
+
+ assert_eq!(
+ captured.borrow()[0],
+ RenderResultPrint {
+ content: "Hello".to_string(),
+ mode: RenderResultMode::Stdout
+ }
+ );
+ assert_eq!(
+ captured.borrow()[1],
+ RenderResultPrint {
+ content: "World\n".to_string(),
+ mode: RenderResultMode::Stderr
+ }
+ );
+ assert_eq!(result.to_string(), "HelloWorld");
+ }
+
+ #[test]
+ fn immediate_output_binds_stdout_hook() {
+ let mut result = RenderResult::default();
+ assert!(result.print_hook.is_none());
+ result.immediate_output();
+ assert!(result.print_hook.is_some());
+ }
+
+ #[test]
+ fn append_other_emits_through_hooks_when_self_has_them() {
+ let mut dest = RenderResult::default();
+ let emitted: Rc<RefCell<Vec<String>>> = Rc::default();
+ let hook_emitted = Rc::clone(&emitted);
+ dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content));
+
+ let mut src = RenderResult::default();
+ src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ dest.append_other(src);
+
+ assert_eq!(emitted.borrow().as_slice(), ["Hello"]);
+ assert_eq!(dest.to_string(), "Hello");
+ }
+
+ #[test]
+ fn append_other_does_not_reemit_when_other_has_hooks() {
+ let mut dest = RenderResult::default();
+ let emitted: Rc<RefCell<Vec<String>>> = Rc::default();
+ let hook_emitted = Rc::clone(&emitted);
+ dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content));
+
+ let mut src = RenderResult::default();
+ src.bind_print_hook(|_| {});
+ src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ dest.append_other(src);
+
+ assert!(emitted.borrow().is_empty());
+ assert_eq!(dest.to_string(), "Hello");
+ }
+
+ #[test]
+ fn write_does_not_emit_through_hooks() {
+ let mut result = RenderResult::default();
+ result.bind_print_hook(|_| panic!("append_to_buffer must not emit"));
+
+ IoWrite::write(&mut result, b"Hello").unwrap();
+ assert_eq!(result.to_string(), "Hello");
+ }
}
diff --git a/mingling_core/src/utils.rs b/mingling_core/src/utils.rs
new file mode 100644
index 0000000..89fcd8f
--- /dev/null
+++ b/mingling_core/src/utils.rs
@@ -0,0 +1,3 @@
+mod splitter;
+
+pub use splitter::ArgumentSplitter;
diff --git a/mingling_core/src/program/repl_exec/splitter.rs b/mingling_core/src/utils/splitter.rs
index 312ad73..6b2ea11 100644
--- a/mingling_core/src/program/repl_exec/splitter.rs
+++ b/mingling_core/src/utils/splitter.rs
@@ -1,12 +1,56 @@
-// Doc Not Optimize
-/// Wraps `split_input` to work with owned `String` inputs.
-pub fn split_input_string(input: &str) -> Vec<String> {
- split_input(input)
+/// A trait for splitting strings into arguments, respecting quotes and escapes.
+///
+/// # Examples
+///
+/// ```
+/// use mingling_core::utils::ArgumentSplitter;
+///
+/// let args = "echo \"hello world\"".split_args();
+/// assert_eq!(args, vec!["echo", "hello world"]);
+/// ```
+pub trait ArgumentSplitter {
+ /// Splits the input string into a vector of argument strings.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = "a 'b c' d".split_args();
+ /// assert_eq!(args, vec!["a", "b c", "d"]);
+ /// ```
+ fn split_args(self) -> Vec<String>;
+}
+
+impl<S: AsRef<str>> ArgumentSplitter for S {
+ /// Splits the string into arguments, respecting single quotes, double
+ /// quotes, and backslash escaping.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = r#"cmd --flag "value with spaces""#.split_args();
+ /// assert_eq!(args, vec!["cmd", "--flag", "value with spaces"]);
+ /// ```
+ ///
+ /// Escaped characters are unescaped:
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = r#"echo a\ b"#.split_args();
+ /// assert_eq!(args, vec!["echo", "a b"]);
+ /// ```
+ fn split_args(self) -> Vec<String> {
+ split_args(self.as_ref())
+ }
}
/// Splits a string input into arguments, respecting single quotes, double quotes,
/// and backslash escaping.
-pub fn split_input(input: &str) -> Vec<String> {
+fn split_args(input: &str) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
let mut current = String::new();
let mut chars = input.chars();
@@ -63,72 +107,72 @@ pub fn split_input(input: &str) -> Vec<String> {
#[cfg(test)]
mod splitter_tests {
- use crate::program::repl_exec::splitter::split_input;
+ use crate::utils::splitter::split_args;
#[test]
fn test_split_with_double_quotes() {
let input = r#"a "b c" d"#;
- let result = split_input(input);
+ let result = split_args(input);
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_split_with_single_quotes() {
let input = "a 'b c' d";
- let result = split_input(input);
+ let result = split_args(input);
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_empty_input() {
- assert!(split_input("").is_empty());
+ assert!(split_args("").is_empty());
}
#[test]
fn test_no_quotes() {
- let result = split_input("hello world");
+ let result = split_args("hello world");
assert_eq!(result, vec!["hello", "world"]);
}
#[test]
fn test_double_quotes_at_edges() {
- let result = split_input(r#""hello world" foo"#);
+ let result = split_args(r#""hello world" foo"#);
assert_eq!(result, vec!["hello world", "foo"]);
}
#[test]
fn test_single_quotes_at_edges() {
- let result = split_input("'hello world' foo");
+ let result = split_args("'hello world' foo");
assert_eq!(result, vec!["hello world", "foo"]);
}
#[test]
fn test_multiple_double_quoted_parts() {
- let result = split_input(r#"a "b c" d "e f g""#);
+ let result = split_args(r#"a "b c" d "e f g""#);
assert_eq!(result, vec!["a", "b c", "d", "e f g"]);
}
#[test]
fn test_multiple_single_quoted_parts() {
- let result = split_input("a 'b c' d 'e f g'");
+ let result = split_args("a 'b c' d 'e f g'");
assert_eq!(result, vec!["a", "b c", "d", "e f g"]);
}
#[test]
fn test_backslash_escaped_space() {
- let result = split_input("a b\\ c d");
+ let result = split_args("a b\\ c d");
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_backslash_escaped_double_quote() {
- let result = split_input(r#"a b\"c d"#);
+ let result = split_args(r#"a b\"c d"#);
assert_eq!(result, vec!["a", r#"b"c"#, "d"]);
}
#[test]
fn test_backslash_escaped_single_quote() {
- let result = split_input("a b\\'c d");
+ let result = split_args("a b\\'c d");
assert_eq!(result, vec!["a", "b'c", "d"]);
}
}