summaryrefslogtreecommitdiff
path: root/mingling_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src')
-rw-r--r--mingling_core/src/any.rs128
-rw-r--r--mingling_core/src/asset.rs4
-rw-r--r--mingling_core/src/asset/chain.rs8
-rw-r--r--mingling_core/src/asset/chain/error.rs13
-rw-r--r--mingling_core/src/asset/dispatcher.rs195
-rw-r--r--mingling_core/src/asset/node.rs54
-rw-r--r--mingling_core/src/asset/renderer.rs6
-rw-r--r--mingling_core/src/lib.rs39
-rw-r--r--mingling_core/src/program.rs132
-rw-r--r--mingling_core/src/program/config.rs26
-rw-r--r--mingling_core/src/program/exec.rs126
-rw-r--r--mingling_core/src/program/exec/error.rs46
-rw-r--r--mingling_core/src/program/flag.rs148
-rw-r--r--mingling_core/src/program/hint.rs62
-rw-r--r--mingling_core/src/program/setup.rs19
-rw-r--r--mingling_core/src/program/setup/basic.rs31
-rw-r--r--mingling_core/src/renderer.rs1
-rw-r--r--mingling_core/src/renderer/render_result.rs38
18 files changed, 1076 insertions, 0 deletions
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs
new file mode 100644
index 0000000..1bce96a
--- /dev/null
+++ b/mingling_core/src/any.rs
@@ -0,0 +1,128 @@
+#[cfg(feature = "general_renderer")]
+use serde::Serialize;
+
+use crate::error::ChainProcessError;
+
+#[derive(Debug)]
+pub struct AnyOutput {
+ inner: Box<dyn std::any::Any + Send + 'static>,
+ pub type_id: std::any::TypeId,
+}
+
+impl AnyOutput {
+ #[cfg(feature = "general_renderer")]
+ pub fn new<T>(value: T) -> Self
+ where
+ T: Send + Serialize + 'static,
+ {
+ Self {
+ inner: Box::new(value),
+ type_id: std::any::TypeId::of::<T>(),
+ }
+ }
+
+ #[cfg(not(feature = "general_renderer"))]
+ pub fn new<T>(value: T) -> Self
+ where
+ T: Send + 'static,
+ {
+ Self {
+ inner: Box::new(value),
+ type_id: std::any::TypeId::of::<T>(),
+ }
+ }
+
+ pub fn downcast<T: 'static>(self) -> Result<T, Self> {
+ if self.type_id == std::any::TypeId::of::<T>() {
+ Ok(*self.inner.downcast::<T>().unwrap())
+ } else {
+ Err(self)
+ }
+ }
+
+ pub fn is<T: 'static>(&self) -> bool {
+ self.type_id == std::any::TypeId::of::<T>()
+ }
+
+ /// Route the output to the next Chain
+ pub fn route_chain(self) -> ChainProcess {
+ ChainProcess::Ok((self, Next::Chain))
+ }
+
+ /// Route the output to the Renderer, ending execution
+ pub fn route_renderer(self) -> ChainProcess {
+ ChainProcess::Ok((self, Next::Renderer))
+ }
+}
+
+impl std::ops::Deref for AnyOutput {
+ type Target = dyn std::any::Any + Send + 'static;
+
+ fn deref(&self) -> &Self::Target {
+ &*self.inner
+ }
+}
+
+impl std::ops::DerefMut for AnyOutput {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut *self.inner
+ }
+}
+
+pub enum ChainProcess {
+ Ok((AnyOutput, Next)),
+ Err(ChainProcessError),
+}
+
+pub enum Next {
+ Chain,
+ Renderer,
+}
+
+impl ChainProcess {
+ pub fn is_next(&self) -> bool {
+ matches!(self, Self::Ok(_))
+ }
+
+ pub fn is_err(&self) -> bool {
+ matches!(self, Self::Err(_))
+ }
+
+ pub fn next(&self) -> Option<&Next> {
+ match self {
+ Self::Ok((_, next)) => Some(next),
+ Self::Err(_) => None,
+ }
+ }
+
+ pub fn err(&self) -> Option<&ChainProcessError> {
+ match self {
+ Self::Ok(_) => None,
+ Self::Err(err) => Some(err),
+ }
+ }
+
+ pub fn unwrap(self) -> (AnyOutput, Next) {
+ match self {
+ Self::Ok(tuple) => tuple,
+ Self::Err(_) => panic!("called `ChainProcess2::unwrap()` on an `Error` value"),
+ }
+ }
+
+ pub fn unwrap_or(self, default: (AnyOutput, Next)) -> (AnyOutput, Next) {
+ match self {
+ Self::Ok(tuple) => tuple,
+ Self::Err(_) => default,
+ }
+ }
+
+ pub fn unwrap_or_else<F>(self, f: F) -> (AnyOutput, Next)
+ where
+ F: FnOnce(ChainProcessError) -> (AnyOutput, Next),
+ {
+ match self {
+ Self::Ok(tuple) => tuple,
+ Self::Err(err) => f(err),
+ }
+ }
+}
diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs
new file mode 100644
index 0000000..c2adf4e
--- /dev/null
+++ b/mingling_core/src/asset.rs
@@ -0,0 +1,4 @@
+pub mod chain;
+pub mod dispatcher;
+pub mod node;
+pub mod renderer;
diff --git a/mingling_core/src/asset/chain.rs b/mingling_core/src/asset/chain.rs
new file mode 100644
index 0000000..1ea1125
--- /dev/null
+++ b/mingling_core/src/asset/chain.rs
@@ -0,0 +1,8 @@
+use crate::ChainProcess;
+
+pub mod error;
+
+pub trait Chain {
+ type Previous;
+ fn proc(p: Self::Previous) -> impl Future<Output = ChainProcess> + Send;
+}
diff --git a/mingling_core/src/asset/chain/error.rs b/mingling_core/src/asset/chain/error.rs
new file mode 100644
index 0000000..d4da4ac
--- /dev/null
+++ b/mingling_core/src/asset/chain/error.rs
@@ -0,0 +1,13 @@
+use crate::AnyOutput;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ChainProcessError {
+ #[error("Other error: {0}")]
+ Other(String),
+
+ #[error("IO error: {0}")]
+ IO(#[from] std::io::Error),
+
+ #[error("Broken chain")]
+ Broken(AnyOutput),
+}
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
new file mode 100644
index 0000000..13e35f7
--- /dev/null
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -0,0 +1,195 @@
+use crate::{ChainProcess, Program, asset::node::Node};
+
+pub trait Dispatcher {
+ fn node(&self) -> Node;
+ fn begin(&self, args: Vec<String>) -> ChainProcess;
+ fn clone_dispatcher(&self) -> Box<dyn Dispatcher>;
+}
+
+impl Clone for Box<dyn Dispatcher> {
+ fn clone(&self) -> Self {
+ self.clone_dispatcher()
+ }
+}
+
+impl<C: crate::program::ProgramCollect> Program<C> {
+ /// Adds a dispatcher to the program.
+ pub fn with_dispatcher<D>(&mut self, dispatcher: D)
+ where
+ D: Into<Dispatchers>,
+ {
+ let dispatchers = dispatcher.into().dispatcher;
+ self.dispatcher.extend(dispatchers);
+ }
+}
+
+pub struct Dispatchers {
+ dispatcher: Vec<Box<dyn Dispatcher + 'static>>,
+}
+
+impl<D> From<D> for Dispatchers
+where
+ D: Dispatcher + 'static,
+{
+ fn from(dispatcher: D) -> Self {
+ Self {
+ dispatcher: vec![Box::new(dispatcher)],
+ }
+ }
+}
+
+impl From<Vec<Box<dyn Dispatcher>>> for Dispatchers {
+ fn from(dispatcher: Vec<Box<dyn Dispatcher>>) -> Self {
+ Self { dispatcher }
+ }
+}
+
+impl From<Box<dyn Dispatcher>> for Dispatchers {
+ fn from(dispatcher: Box<dyn Dispatcher>) -> Self {
+ Self {
+ dispatcher: vec![dispatcher],
+ }
+ }
+}
+
+impl<D> From<(D,)> for Dispatchers
+where
+ D: Dispatcher + 'static,
+{
+ fn from(dispatcher: (D,)) -> Self {
+ Self {
+ dispatcher: vec![Box::new(dispatcher.0)],
+ }
+ }
+}
+
+impl<D1, D2> From<(D1, D2)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2)) -> Self {
+ Self {
+ dispatcher: vec![Box::new(dispatchers.0), Box::new(dispatchers.1)],
+ }
+ }
+}
+
+impl<D1, D2, D3> From<(D1, D2, D3)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+ D3: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2, D3)) -> Self {
+ Self {
+ dispatcher: vec![
+ Box::new(dispatchers.0),
+ Box::new(dispatchers.1),
+ Box::new(dispatchers.2),
+ ],
+ }
+ }
+}
+
+impl<D1, D2, D3, D4> From<(D1, D2, D3, D4)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+ D3: Dispatcher + 'static,
+ D4: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2, D3, D4)) -> Self {
+ Self {
+ dispatcher: vec![
+ Box::new(dispatchers.0),
+ Box::new(dispatchers.1),
+ Box::new(dispatchers.2),
+ Box::new(dispatchers.3),
+ ],
+ }
+ }
+}
+
+impl<D1, D2, D3, D4, D5> From<(D1, D2, D3, D4, D5)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+ D3: Dispatcher + 'static,
+ D4: Dispatcher + 'static,
+ D5: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2, D3, D4, D5)) -> Self {
+ Self {
+ dispatcher: vec![
+ Box::new(dispatchers.0),
+ Box::new(dispatchers.1),
+ Box::new(dispatchers.2),
+ Box::new(dispatchers.3),
+ Box::new(dispatchers.4),
+ ],
+ }
+ }
+}
+
+impl<D1, D2, D3, D4, D5, D6> From<(D1, D2, D3, D4, D5, D6)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+ D3: Dispatcher + 'static,
+ D4: Dispatcher + 'static,
+ D5: Dispatcher + 'static,
+ D6: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2, D3, D4, D5, D6)) -> Self {
+ Self {
+ dispatcher: vec![
+ Box::new(dispatchers.0),
+ Box::new(dispatchers.1),
+ Box::new(dispatchers.2),
+ Box::new(dispatchers.3),
+ Box::new(dispatchers.4),
+ Box::new(dispatchers.5),
+ ],
+ }
+ }
+}
+
+impl<D1, D2, D3, D4, D5, D6, D7> From<(D1, D2, D3, D4, D5, D6, D7)> for Dispatchers
+where
+ D1: Dispatcher + 'static,
+ D2: Dispatcher + 'static,
+ D3: Dispatcher + 'static,
+ D4: Dispatcher + 'static,
+ D5: Dispatcher + 'static,
+ D6: Dispatcher + 'static,
+ D7: Dispatcher + 'static,
+{
+ fn from(dispatchers: (D1, D2, D3, D4, D5, D6, D7)) -> Self {
+ Self {
+ dispatcher: vec![
+ Box::new(dispatchers.0),
+ Box::new(dispatchers.1),
+ Box::new(dispatchers.2),
+ Box::new(dispatchers.3),
+ Box::new(dispatchers.4),
+ Box::new(dispatchers.5),
+ Box::new(dispatchers.6),
+ ],
+ }
+ }
+}
+
+impl std::ops::Deref for Dispatchers {
+ type Target = Vec<Box<dyn Dispatcher + 'static>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.dispatcher
+ }
+}
+
+impl From<Dispatchers> for Vec<Box<dyn Dispatcher + 'static>> {
+ fn from(val: Dispatchers) -> Self {
+ val.dispatcher
+ }
+}
diff --git a/mingling_core/src/asset/node.rs b/mingling_core/src/asset/node.rs
new file mode 100644
index 0000000..c8b7600
--- /dev/null
+++ b/mingling_core/src/asset/node.rs
@@ -0,0 +1,54 @@
+use just_fmt::kebab_case;
+
+#[derive(Debug, Default)]
+pub struct Node {
+ node: Vec<String>,
+}
+
+impl Node {
+ pub fn join(self, node: impl Into<String>) -> Node {
+ let mut new_node = self.node;
+ new_node.push(node.into());
+ Node { node: new_node }
+ }
+}
+
+impl From<&str> for Node {
+ fn from(s: &str) -> Self {
+ let node = s.split('.').map(|part| kebab_case!(part)).collect();
+ Node { node }
+ }
+}
+
+impl From<String> for Node {
+ fn from(s: String) -> Self {
+ let node = s.split('.').map(|part| kebab_case!(part)).collect();
+ Node { node }
+ }
+}
+
+impl PartialEq for Node {
+ fn eq(&self, other: &Self) -> bool {
+ self.node == other.node
+ }
+}
+
+impl Eq for Node {}
+
+impl PartialOrd for Node {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for Node {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.node.cmp(&other.node)
+ }
+}
+
+impl std::fmt::Display for Node {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.node.join("."))
+ }
+}
diff --git a/mingling_core/src/asset/renderer.rs b/mingling_core/src/asset/renderer.rs
new file mode 100644
index 0000000..3852b55
--- /dev/null
+++ b/mingling_core/src/asset/renderer.rs
@@ -0,0 +1,6 @@
+use crate::RenderResult;
+
+pub trait Renderer {
+ type Previous;
+ fn render(p: Self::Previous, r: &mut RenderResult);
+}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
new file mode 100644
index 0000000..4d9bcc5
--- /dev/null
+++ b/mingling_core/src/lib.rs
@@ -0,0 +1,39 @@
+mod any;
+pub use crate::any::*;
+
+pub mod error {
+ pub use crate::asset::chain::error::*;
+ pub use crate::exec::error::*;
+}
+
+mod program;
+pub use crate::program::*;
+pub mod setup {
+ pub use crate::program::setup::*;
+}
+pub mod hint {
+ pub use crate::program::hint::*;
+}
+
+#[cfg(feature = "macros")]
+#[allow(unused_imports)]
+pub mod macros {
+ pub use mingling_macros::chain;
+ pub use mingling_macros::chain_struct;
+ pub use mingling_macros::dispatcher;
+ pub use mingling_macros::dispatcher_render;
+ pub use mingling_macros::node;
+ pub use mingling_macros::program;
+ pub use mingling_macros::r_print;
+ pub use mingling_macros::r_println;
+ pub use mingling_macros::renderer;
+}
+
+mod renderer;
+
+mod asset;
+pub use crate::asset::chain::*;
+pub use crate::asset::dispatcher::*;
+pub use crate::asset::node::*;
+pub use crate::asset::renderer::*;
+pub use crate::renderer::render_result::*;
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
new file mode 100644
index 0000000..3d8bc14
--- /dev/null
+++ b/mingling_core/src/program.rs
@@ -0,0 +1,132 @@
+use crate::{
+ AnyOutput, ChainProcess, RenderResult, asset::dispatcher::Dispatcher,
+ error::ProgramExecuteError,
+};
+use std::{env, pin::Pin};
+
+pub mod exec;
+pub mod hint;
+pub mod setup;
+
+mod config;
+pub use config::*;
+
+mod flag;
+pub use flag::*;
+use tokio::io::AsyncWriteExt;
+
+#[derive(Default)]
+pub struct Program<C: ProgramCollect> {
+ pub(crate) collect: std::marker::PhantomData<C>,
+
+ pub(crate) args: Vec<String>,
+ pub(crate) dispatcher: Vec<Box<dyn Dispatcher>>,
+
+ pub stdout_setting: ProgramStdoutSetting,
+ pub user_context: ProgramUserContext,
+}
+
+impl<C> Program<C>
+where
+ C: ProgramCollect,
+{
+ /// Creates a new Program instance, initializing args from environment.
+ pub fn new() -> Self {
+ Program {
+ collect: std::marker::PhantomData,
+ args: env::args().collect(),
+ dispatcher: Vec::new(),
+ stdout_setting: Default::default(),
+ user_context: Default::default(),
+ }
+ }
+
+ /// Run the command line program
+ pub async fn exec_without_render(mut self) -> Result<RenderResult, ProgramExecuteError> {
+ self.args = self.args.iter().skip(1).cloned().collect();
+ crate::exec::exec(self).await.map_err(|e| e.into())
+ }
+
+ /// Run the command line program
+ pub async fn exec(self) {
+ let stdout_setting = self.stdout_setting.clone();
+ let result = match self.exec_without_render().await {
+ Ok(r) => r,
+ Err(e) => match e {
+ ProgramExecuteError::DispatcherNotFound => {
+ eprintln!("Dispatcher not found");
+ return;
+ }
+ ProgramExecuteError::Other(e) => {
+ eprintln!("{}", e);
+ return;
+ }
+ },
+ };
+
+ // Render result
+ if stdout_setting.render_output && !result.is_empty() {
+ print!("{}", result);
+ if let Err(e) = tokio::io::stdout().flush().await
+ && stdout_setting.error_output
+ {
+ eprintln!("{}", e);
+ }
+ }
+ }
+}
+
+pub trait ProgramCollect {
+ fn render(any: AnyOutput, r: &mut RenderResult);
+ fn do_chain(any: AnyOutput) -> Pin<Box<dyn Future<Output = ChainProcess> + Send>>;
+ fn has_renderer(any: &AnyOutput) -> bool;
+ fn has_chain(any: &AnyOutput) -> bool;
+}
+
+#[macro_export]
+#[doc(hidden)]
+macro_rules! __dispatch_program_renderers {
+ (
+ $( $render_ty:ty => $prev_ty:ty, )*
+ ) => {
+ fn render(any: mingling::AnyOutput, r: &mut mingling::RenderResult) {
+ match any.type_id {
+ $(
+ id if id == std::any::TypeId::of::<$prev_ty>() => {
+ let value = any.downcast::<$prev_ty>().unwrap();
+ <$render_ty as mingling::Renderer>::render(value, r);
+ }
+ )*
+ _ => (),
+ }
+ }
+ };
+}
+
+#[macro_export]
+#[doc(hidden)]
+macro_rules! __dispatch_program_chains {
+ (
+ $( $chain_ty:ty => $chain_prev:ty, )*
+ ) => {
+ fn do_chain(
+ any: mingling::AnyOutput,
+ ) -> std::pin::Pin<Box<dyn Future<Output = mingling::ChainProcess> + Send>> {
+ match any.type_id {
+ $(
+ id if id == std::any::TypeId::of::<$chain_prev>() => {
+ let value = any.downcast::<$chain_prev>().unwrap();
+ let fut = async { <$chain_ty as mingling::Chain>::proc(value).await };
+ Box::pin(fut)
+ }
+ )*
+ _ => Box::pin(async move {
+ mingling::AnyOutput::new(mingling::hint::NoChainFound {
+ name: format!("{:?}", any.type_id).to_string(),
+ })
+ .route_chain()
+ }),
+ }
+ }
+ };
+}
diff --git a/mingling_core/src/program/config.rs b/mingling_core/src/program/config.rs
new file mode 100644
index 0000000..386b112
--- /dev/null
+++ b/mingling_core/src/program/config.rs
@@ -0,0 +1,26 @@
+#[derive(Debug, Clone)]
+pub struct ProgramStdoutSetting {
+ /// Output error messages
+ pub error_output: bool,
+
+ /// Render results and output
+ pub render_output: bool,
+}
+
+impl Default for ProgramStdoutSetting {
+ fn default() -> Self {
+ ProgramStdoutSetting {
+ error_output: true,
+ render_output: true,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct ProgramUserContext {
+ /// View help information instead of running the command
+ pub help: bool,
+
+ /// Skip user confirmation step
+ pub confirm: bool,
+}
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
new file mode 100644
index 0000000..853bff1
--- /dev/null
+++ b/mingling_core/src/program/exec.rs
@@ -0,0 +1,126 @@
+#![allow(clippy::borrowed_box)]
+
+use crate::{
+ AnyOutput, ChainProcess, Dispatcher, Next, Program, ProgramCollect, RenderResult,
+ error::ProgramInternalExecuteError,
+ hint::{DispatcherNotFound, RendererNotFound},
+};
+
+pub mod error;
+
+pub async fn exec<C: ProgramCollect>(
+ program: Program<C>,
+) -> Result<RenderResult, ProgramInternalExecuteError> {
+ // Match user input
+ let matched: (Box<dyn Dispatcher>, Vec<String>) = match match_user_input(&program) {
+ Ok(r) => (r.0.clone(), r.1),
+ Err(ProgramInternalExecuteError::DispatcherNotFound) => {
+ // If no Dispatcher is found, dispatch to the DispatcherNotFound Dispatcher
+ // to route it to the NoDispatcherFound struct
+ let disp: Box<dyn Dispatcher> = Box::new(DispatcherNotFound);
+ (disp, program.args)
+ }
+ Err(e) => return Err(e),
+ };
+
+ // Entry point
+ let (dispatcher, args) = matched;
+ let mut current = match dispatcher.begin(args) {
+ ChainProcess::Ok((any, Next::Renderer)) => return Ok(render::<C>(any)),
+ ChainProcess::Ok((any, Next::Chain)) => any,
+ ChainProcess::Err(e) => return Err(e.into()),
+ };
+
+ loop {
+ current = {
+ // If a chain exists, execute as a chain
+ if C::has_chain(&current) {
+ match C::do_chain(current).await {
+ ChainProcess::Ok((any, Next::Renderer)) => return Ok(render::<C>(any)),
+ ChainProcess::Ok((any, Next::Chain)) => any,
+ ChainProcess::Err(e) => return Err(e.into()),
+ }
+ }
+ // If no chain exists, attempt to render
+ else if C::has_renderer(&current) {
+ let mut render_result = RenderResult::default();
+ C::render(current, &mut render_result);
+ return Ok(render_result);
+ }
+ // If no renderer exists, transfer to the RendererNotFound Dispatcher for execution
+ else {
+ let disp: Box<dyn Dispatcher> = Box::new(RendererNotFound);
+
+ match disp.begin(vec![format!("{:?}", current.type_id)]) {
+ ChainProcess::Ok((any, Next::Renderer)) => return Ok(render::<C>(any)),
+ ChainProcess::Ok((any, Next::Chain)) => any,
+ ChainProcess::Err(e) => return Err(e.into()),
+ }
+ }
+ };
+ }
+}
+
+/// Match user input against registered dispatchers and return the matched dispatcher and remaining arguments.
+fn match_user_input<C: ProgramCollect>(
+ program: &Program<C>,
+) -> Result<(&Box<dyn Dispatcher>, Vec<String>), ProgramInternalExecuteError> {
+ let nodes = get_nodes(program);
+ let command = format!("{} ", program.args.join(" "));
+
+ // Find all nodes that match the command prefix
+ let matching_nodes: Vec<&(String, &Box<dyn Dispatcher>)> = nodes
+ .iter()
+ // Also add a space to the node string to ensure consistent matching logic
+ .filter(|(node_str, _)| command.starts_with(&format!("{} ", node_str)))
+ .collect();
+
+ match matching_nodes.len() {
+ 0 => {
+ // No matching node found
+ Err(ProgramInternalExecuteError::DispatcherNotFound)
+ }
+ 1 => {
+ let matched_prefix = matching_nodes[0];
+ let prefix_len = matched_prefix.0.split_whitespace().count();
+ let trimmed_args: Vec<String> = program.args.iter().skip(prefix_len).cloned().collect();
+ Ok((matched_prefix.1, trimmed_args))
+ }
+ _ => {
+ // Multiple matching nodes found
+ // Find the node with the longest length (most specific match)
+ let matched_prefix = matching_nodes
+ .iter()
+ .max_by_key(|node| node.0.len())
+ .unwrap();
+
+ let prefix_len = matched_prefix.0.split_whitespace().count();
+ let trimmed_args: Vec<String> = program.args.iter().skip(prefix_len).cloned().collect();
+ Ok((matched_prefix.1, trimmed_args))
+ }
+ }
+}
+
+#[inline(always)]
+fn render<C: ProgramCollect>(any: AnyOutput) -> RenderResult {
+ let mut render_result = RenderResult::default();
+ C::render(any, &mut render_result);
+ render_result
+}
+
+// Get all registered dispatcher names from the program
+fn get_nodes<C: ProgramCollect>(program: &Program<C>) -> Vec<(String, &Box<dyn Dispatcher>)> {
+ program
+ .dispatcher
+ .iter()
+ .map(|disp| {
+ let node_str = disp
+ .node()
+ .to_string()
+ .split('.')
+ .collect::<Vec<_>>()
+ .join(" ");
+ (node_str, disp)
+ })
+ .collect()
+}
diff --git a/mingling_core/src/program/exec/error.rs b/mingling_core/src/program/exec/error.rs
new file mode 100644
index 0000000..fe66e22
--- /dev/null
+++ b/mingling_core/src/program/exec/error.rs
@@ -0,0 +1,46 @@
+use crate::error::ChainProcessError;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ProgramExecuteError {
+ #[error("No Dispatcher Found")]
+ DispatcherNotFound,
+
+ #[error("Other error: {0}")]
+ Other(String),
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum ProgramInternalExecuteError {
+ #[error("No Dispatcher Found")]
+ DispatcherNotFound,
+
+ #[error("Other error: {0}")]
+ Other(String),
+
+ #[error("IO error: {0}")]
+ IO(#[from] std::io::Error),
+}
+
+impl From<ProgramInternalExecuteError> for ProgramExecuteError {
+ fn from(value: ProgramInternalExecuteError) -> Self {
+ match value {
+ ProgramInternalExecuteError::DispatcherNotFound => {
+ ProgramExecuteError::DispatcherNotFound
+ }
+ ProgramInternalExecuteError::Other(s) => ProgramExecuteError::Other(s),
+ ProgramInternalExecuteError::IO(e) => ProgramExecuteError::Other(format!("{}", e)),
+ }
+ }
+}
+
+impl From<ChainProcessError> for ProgramInternalExecuteError {
+ fn from(value: ChainProcessError) -> Self {
+ match value {
+ ChainProcessError::Other(s) => ProgramInternalExecuteError::Other(s),
+ ChainProcessError::IO(error) => ProgramInternalExecuteError::IO(error),
+ ChainProcessError::Broken(_) => {
+ ProgramInternalExecuteError::Other("Broken".to_string())
+ }
+ }
+ }
+}
diff --git a/mingling_core/src/program/flag.rs b/mingling_core/src/program/flag.rs
new file mode 100644
index 0000000..3a678be
--- /dev/null
+++ b/mingling_core/src/program/flag.rs
@@ -0,0 +1,148 @@
+use crate::{Program, ProgramCollect};
+
+pub struct Flag {
+ vec: Vec<&'static str>,
+}
+
+impl From<&'static str> for Flag {
+ fn from(s: &'static str) -> Self {
+ Flag { vec: vec![s] }
+ }
+}
+
+impl From<&'static [&'static str]> for Flag {
+ fn from(slice: &'static [&'static str]) -> Self {
+ Flag {
+ vec: slice.to_vec(),
+ }
+ }
+}
+
+impl<const N: usize> From<[&'static str; N]> for Flag {
+ fn from(slice: [&'static str; N]) -> Self {
+ Flag {
+ vec: slice.to_vec(),
+ }
+ }
+}
+
+impl<const N: usize> From<&'static [&'static str; N]> for Flag {
+ fn from(slice: &'static [&'static str; N]) -> Self {
+ Flag {
+ vec: slice.to_vec(),
+ }
+ }
+}
+
+impl AsRef<[&'static str]> for Flag {
+ fn as_ref(&self) -> &[&'static str] {
+ &self.vec
+ }
+}
+
+impl std::ops::Deref for Flag {
+ type Target = [&'static str];
+
+ fn deref(&self) -> &Self::Target {
+ &self.vec
+ }
+}
+
+macro_rules! special_flag {
+ ($args:expr, $flag:expr) => {{
+ let flag = $flag;
+ let found = $args.iter().any(|arg| arg == flag);
+ $args.retain(|arg| arg != flag);
+ found
+ }};
+}
+
+macro_rules! special_argument {
+ ($args:expr, $flag:expr) => {{
+ let flag = $flag;
+ let mut value: Option<String> = None;
+ let mut i = 0;
+ while i < $args.len() {
+ if &$args[i] == flag {
+ if i + 1 < $args.len() {
+ value = Some($args[i + 1].clone());
+ $args.remove(i + 1);
+ $args.remove(i);
+ } else {
+ value = None;
+ $args.remove(i);
+ }
+ break;
+ }
+ i += 1;
+ }
+ value
+ }};
+}
+
+impl<C> Program<C>
+where
+ C: ProgramCollect,
+{
+ /// Registers a global argument (with value) and its handler.
+ pub fn global_argument<F, A>(&mut self, arguments: A, do_fn: F)
+ where
+ F: Fn(&mut Program<C>, String),
+ A: Into<Flag>,
+ {
+ let flag = arguments.into();
+ for argument in flag.iter() {
+ let value = special_argument!(self.args, argument);
+ if let Some(value) = value {
+ do_fn(self, value);
+ return;
+ }
+ }
+ }
+
+ /// Registers a global flag (boolean) and its handler.
+ pub fn global_flag<F, A>(&mut self, flag: A, do_fn: F)
+ where
+ F: Fn(&mut Program<C>),
+ A: Into<Flag>,
+ {
+ let flag = flag.into();
+ for argument in flag.iter() {
+ let enabled = special_flag!(self.args, argument);
+ if enabled {
+ do_fn(self);
+ return;
+ }
+ }
+ }
+
+ /// Extracts a global argument (with value) from arguments
+ pub fn pick_global_argument<F>(&mut self, flag: F) -> Option<String>
+ where
+ F: Into<Flag>,
+ {
+ let flag: Flag = flag.into();
+ for argument in flag.iter() {
+ let value = special_argument!(self.args, argument);
+ if value.is_some() {
+ return value;
+ }
+ }
+ None
+ }
+
+ /// Extracts global flags from arguments
+ pub fn pick_global_flag<F>(&mut self, flag: F) -> bool
+ where
+ F: Into<Flag>,
+ {
+ let flag: Flag = flag.into();
+ for argument in flag.iter() {
+ let enabled = special_flag!(self.args, argument);
+ if enabled {
+ return enabled;
+ }
+ }
+ false
+ }
+}
diff --git a/mingling_core/src/program/hint.rs b/mingling_core/src/program/hint.rs
new file mode 100644
index 0000000..6dbbac2
--- /dev/null
+++ b/mingling_core/src/program/hint.rs
@@ -0,0 +1,62 @@
+use crate::{AnyOutput, ChainProcess, Dispatcher, Node};
+
+/// Marker: Dispatcher Not Found
+///
+/// If a Dispatcher outputs NoDispatcherFound to the Chain,
+/// the program will terminate directly.
+///
+/// You can implement Renderer for NoDispatcherFound
+/// to render relevant information when a Dispatcher cannot be found.
+#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+pub struct NoDispatcherFound {
+ pub args: Vec<String>,
+}
+
+#[derive(Default)]
+#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+pub struct DispatcherNotFound;
+impl Dispatcher for DispatcherNotFound {
+ fn node(&self) -> crate::Node {
+ Node::default().join("_not_found")
+ }
+
+ fn begin(&self, args: Vec<String>) -> ChainProcess {
+ AnyOutput::new(NoDispatcherFound { args }).route_renderer()
+ }
+
+ fn clone_dispatcher(&self) -> Box<dyn Dispatcher> {
+ Box::new(DispatcherNotFound)
+ }
+}
+
+/// Marker: Renderer Not Found
+///
+/// If a Chain outputs NoRendererFound to the Chain,
+/// the program will terminate directly.
+///
+/// You can implement Renderer for NoRendererFound
+/// to render relevant information when a Renderer cannot be found.
+#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+pub struct NoRendererFound {
+ pub type_to_render: String,
+}
+
+#[derive(Default)]
+#[cfg_attr(feature = "general_renderer", derive(serde::Serialize))]
+pub struct RendererNotFound;
+impl Dispatcher for RendererNotFound {
+ fn node(&self) -> crate::Node {
+ Node::default().join("_not_found")
+ }
+
+ fn begin(&self, args: Vec<String>) -> ChainProcess {
+ AnyOutput::new(NoRendererFound {
+ type_to_render: args.first().unwrap().clone(),
+ })
+ .route_renderer()
+ }
+
+ fn clone_dispatcher(&self) -> Box<dyn Dispatcher> {
+ Box::new(RendererNotFound)
+ }
+}
diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs
new file mode 100644
index 0000000..e81247e
--- /dev/null
+++ b/mingling_core/src/program/setup.rs
@@ -0,0 +1,19 @@
+use crate::{ProgramCollect, program::Program};
+
+mod basic;
+pub use basic::*;
+
+pub trait ProgramSetup<C: ProgramCollect> {
+ fn setup(&mut self, program: &mut Program<C>);
+}
+
+impl<C> Program<C>
+where
+ C: ProgramCollect,
+{
+ /// Load and execute init logic
+ pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, mut setup: S) -> S {
+ S::setup(&mut setup, self);
+ setup
+ }
+}
diff --git a/mingling_core/src/program/setup/basic.rs b/mingling_core/src/program/setup/basic.rs
new file mode 100644
index 0000000..43c14b9
--- /dev/null
+++ b/mingling_core/src/program/setup/basic.rs
@@ -0,0 +1,31 @@
+use crate::{
+ ProgramCollect,
+ program::{Program, setup::ProgramSetup},
+};
+
+/// Performs basic program initialization:
+///
+/// - Collects `--quiet` flag to control message rendering
+/// - Collects `--help` flag to enable help mode
+/// - Collects `--confirm` flag to skip user confirmation
+pub struct BasicProgramSetup;
+
+impl<C> ProgramSetup<C> for BasicProgramSetup
+where
+ C: ProgramCollect,
+{
+ fn setup(&mut self, program: &mut Program<C>) {
+ program.global_flag(["--quiet", "-q"], |p| {
+ p.stdout_setting.render_output = false;
+ p.stdout_setting.error_output = false;
+ });
+
+ program.global_flag(["--help", "-h"], |p| {
+ p.user_context.help = true;
+ });
+
+ program.global_flag(["--confirm", "-C"], |p| {
+ p.user_context.confirm = true;
+ });
+ }
+}
diff --git a/mingling_core/src/renderer.rs b/mingling_core/src/renderer.rs
new file mode 100644
index 0000000..631092b
--- /dev/null
+++ b/mingling_core/src/renderer.rs
@@ -0,0 +1 @@
+pub mod render_result;
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
new file mode 100644
index 0000000..73c38e7
--- /dev/null
+++ b/mingling_core/src/renderer/render_result.rs
@@ -0,0 +1,38 @@
+use std::{
+ fmt::{Display, Formatter},
+ ops::Deref,
+};
+
+#[derive(Default, Debug, PartialEq)]
+pub struct RenderResult {
+ render_text: String,
+}
+
+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
+ }
+}
+
+impl RenderResult {
+ pub fn print(&mut self, text: &str) {
+ self.render_text.push_str(text);
+ }
+
+ pub fn println(&mut self, text: &str) {
+ self.render_text.push_str(text);
+ self.render_text.push('\n');
+ }
+
+ pub fn clear(&mut self) {
+ self.render_text.clear();
+ }
+}