aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/asset
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core/src/asset')
-rw-r--r--mingling_core/src/asset/chain.rs44
-rw-r--r--mingling_core/src/asset/chain/error.rs33
-rw-r--r--mingling_core/src/asset/core_invokes.rs40
-rw-r--r--mingling_core/src/asset/dispatcher.rs450
-rw-r--r--mingling_core/src/asset/enum_tag.rs49
-rw-r--r--mingling_core/src/asset/global_resource.rs1606
-rw-r--r--mingling_core/src/asset/help.rs45
-rw-r--r--mingling_core/src/asset/lazy_resource.rs601
-rw-r--r--mingling_core/src/asset/metadata.rs37
-rw-r--r--mingling_core/src/asset/node.rs134
-rw-r--r--mingling_core/src/asset/renderer.rs31
-rw-r--r--mingling_core/src/asset/routable.rs110
12 files changed, 2430 insertions, 750 deletions
diff --git a/mingling_core/src/asset/chain.rs b/mingling_core/src/asset/chain.rs
index bd504d0..5276809 100644
--- a/mingling_core/src/asset/chain.rs
+++ b/mingling_core/src/asset/chain.rs
@@ -3,16 +3,52 @@ use crate::ChainProcess;
#[doc(hidden)]
pub mod error;
-/// Takes over a type (G: Previous) and converts it to another [`AnyOutput`](./struct.AnyOutput.html)
+/// Mingling's program logic execution unit
+///
+/// Binds a chain to a type. When the program is scheduled to that type, the
+/// `proc` function in the chain will be executed to convert it to the next
+/// type and send it to the scheduler.
+///
+/// # Async
+///
+/// When the `async` feature is enabled, the `proc` function of this trait no
+/// longer requires returning a [`ChainProcess`], but rather a Future whose
+/// output is a [`ChainProcess`].
+///
+/// # Manual impl
+///
+/// If you need to implement it manually, please do so as follows:
+///
+/// ```
+/// # use mingling_core::Chain;
+/// # use mingling_core::ChainProcess;
+/// # enum ThisProgram {}
+/// struct MyChain;
+/// struct StateMyType;
+///
+/// impl Chain<ThisProgram> for MyChain {
+/// type Previous = StateMyType;
+///
+/// fn proc(prev: Self::Previous) -> ChainProcess<ThisProgram> {
+/// // Specific type conversion logic
+/// # return mingling_core::ChainProcess::<ThisProgram>::Err(mingling_core::error::ChainProcessError::Other("test".to_string()));
+/// }
+/// }
+/// ```
pub trait Chain<G> {
- /// The previous type in the chain
+ /// The previous type bound to the chain, used to convert to the next arbitrary type in this chain
type Previous;
- /// Process the previous value and return a future that resolves to a [`ChainProcess<G>`](./enum.ChainProcess.html)
#[cfg(feature = "async")]
+ /// The execution logic of the chain, converting the type `Previous` into the next type and returning an asynchronous [`ChainProcess`].
+ ///
+ /// Called when the `async` feature is enabled, this method returns a result that implements `Future`,
+ /// whose output is a [`ChainProcess<G>`].
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"))]
+ /// The execution logic of the chain, converting the type `Previous` into the next type and returning a [`ChainProcess`].
+ ///
+ /// Called when the `async` feature is disabled, this method synchronously returns a [`ChainProcess<G>`].
fn proc(p: Self::Previous) -> ChainProcess<G>;
}
diff --git a/mingling_core/src/asset/chain/error.rs b/mingling_core/src/asset/chain/error.rs
index ad64195..4845dbd 100644
--- a/mingling_core/src/asset/chain/error.rs
+++ b/mingling_core/src/asset/chain/error.rs
@@ -1,20 +1,23 @@
use crate::error::ProgramInternalExecuteError;
-/// Represents errors that can occur during chain processing.
+/// Represents error types that occur in a chained processing pipeline.
+///
+/// This enum is used to uniformly encapsulate various exceptions that may
+/// occur during the execution of an entire chain, including IO errors and
+/// other custom error messages.
#[derive(Debug)]
pub enum ChainProcessError {
- /// An error with a custom description.
+ /// Other unclassified generic errors, stored as a string description.
Other(String),
-
- /// An I/O error that occurred during chain processing.
+ /// Errors resulting from a failed IO operation, holding the standard library's [`std::io::Error`]
IO(std::io::Error),
}
impl std::fmt::Display for ChainProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- ChainProcessError::Other(s) => write!(f, "Other error: {s}"),
- ChainProcessError::IO(e) => write!(f, "IO error: {e}"),
+ Self::Other(s) => write!(f, "Other error: {s}"),
+ Self::IO(e) => write!(f, "IO error: {e}"),
}
}
}
@@ -22,15 +25,15 @@ impl std::fmt::Display for ChainProcessError {
impl std::error::Error for ChainProcessError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
- ChainProcessError::IO(e) => Some(e),
- ChainProcessError::Other(_) => None,
+ Self::IO(e) => Some(e),
+ Self::Other(_) => None,
}
}
}
impl From<std::io::Error> for ChainProcessError {
fn from(e: std::io::Error) -> Self {
- ChainProcessError::IO(e)
+ Self::IO(e)
}
}
@@ -38,17 +41,15 @@ impl From<ProgramInternalExecuteError> for ChainProcessError {
fn from(value: ProgramInternalExecuteError) -> Self {
match value {
ProgramInternalExecuteError::DispatcherNotFound => {
- ChainProcessError::Other("DispatcherNotFound".into())
+ Self::Other("DispatcherNotFound".into())
}
ProgramInternalExecuteError::RendererNotFound(r) => {
- ChainProcessError::Other(format!("RendererNotFound: {r}"))
- }
- ProgramInternalExecuteError::Other(e) => ChainProcessError::Other(e),
- ProgramInternalExecuteError::IO(e) => {
- ChainProcessError::Other(format!("IOError: {e:?}"))
+ Self::Other(format!("RendererNotFound: {r}"))
}
+ ProgramInternalExecuteError::Other(e) => Self::Other(e),
+ ProgramInternalExecuteError::IO(e) => Self::Other(format!("IOError: {e:?}")),
ProgramInternalExecuteError::REPLPanic(program_panic) => {
- ChainProcessError::Other(format!("REPLPanic: {program_panic}"))
+ Self::Other(format!("REPLPanic: {program_panic}"))
}
}
}
diff --git a/mingling_core/src/asset/core_invokes.rs b/mingling_core/src/asset/core_invokes.rs
index 6a3da2d..705568b 100644
--- a/mingling_core/src/asset/core_invokes.rs
+++ b/mingling_core/src/asset/core_invokes.rs
@@ -48,18 +48,17 @@ 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!"
- );
- }
+ assert!(
+ self.create_by_res_injection,
+ "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 {
+ Self {
phantom: PhantomData,
create_by_res_injection: self.create_by_res_injection,
}
@@ -71,7 +70,7 @@ impl<T> ResourceMarker for RendererInvoker<T> {
// 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 {
+ Self {
phantom: PhantomData,
create_by_res_injection: true,
}
@@ -136,6 +135,9 @@ where
///
/// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control.
#[might_be_async::func]
+ #[allow(clippy::future_not_send)]
+ // The generated future is only awaited within the single-threaded chain
+ // execution, so it does not need to be `Send`.
pub fn invoke_once<C>(&self, value: T) -> ChainProcess<C>
where
C: ProgramCollect<Enum = C> + 'static,
@@ -166,6 +168,9 @@ where
///
/// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control.
#[might_be_async::func]
+ // The generated future is only awaited within the single-threaded chain
+ // execution, so it does not need to be `Send`.
+ #[allow(clippy::future_not_send)]
pub fn invoke_to_last<C>(&self, value: T) -> ChainProcess<C>
where
C: ProgramCollect<Enum = C> + 'static,
@@ -190,7 +195,7 @@ where
}
}
- /// Continuously execute the chain until it is rendered into a RenderResult
+ /// 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
@@ -198,7 +203,7 @@ where
///
/// # 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
+ /// If an error occurs during rendering, or the result type does not contain a renderer, it will be rendered as an empty `RenderResult`
///
/// # Panics
///
@@ -208,6 +213,9 @@ where
///
/// It will not execute any program hooks, because this type is used for **bypassing** or **reusing**, not for flow control.
#[might_be_async::func]
+ #[allow(clippy::future_not_send)]
+ // The generated future is only awaited within the single-threaded chain
+ // execution, so it does not need to be `Send`.
pub fn invoke_to_result<C>(&self, value: T) -> RenderResult
where
C: ProgramCollect<Enum = C> + 'static,
@@ -229,19 +237,17 @@ where
}
}
- #[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!"
- );
- }
+ assert!(
+ self.create_by_res_injection,
+ "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 {
+ Self {
phantom: PhantomData,
create_by_res_injection: self.create_by_res_injection,
}
@@ -253,7 +259,7 @@ impl<T> ResourceMarker for ChainInvoker<T> {
// 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 {
+ Self {
phantom: PhantomData,
create_by_res_injection: true,
}
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
index cb0987d..79ebee0 100644
--- a/mingling_core/src/asset/dispatcher.rs
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -1,403 +1,57 @@
-use std::fmt::Display;
+use crate::ChainProcess;
-use crate::{ChainProcess, Program, ProgramCollect, asset::node::Node};
-
-/// Dispatches user input commands to specific [`ChainProcess`](./enum.ChainProcess.html)
+/// The entry logic of the Mingling program
///
-/// Note: If you are using [mingling_macros](https://crates.io/crates/mingling_macros),
-/// you can use the `dispatcher!("node.subnode", CommandType => Entry)` macro to declare a `Dispatcher`
-pub trait Dispatcher<C> {
- /// Returns a command node for matching user input
- fn node(&self) -> Node;
-
- /// Returns a [`ChainProcess`](./enum.ChainProcess.html) based on user input arguments,
- /// to be sent to the specific invocation
- fn begin(&self, args: Vec<String>) -> ChainProcess<C>;
-
- /// Clones the current dispatcher for implementing the `Clone` trait
- fn clone_dispatcher(&self) -> Box<dyn Dispatcher<C>>;
-}
-
-impl<G> Clone for Box<dyn Dispatcher<G>>
-where
- G: Display,
-{
- fn clone(&self) -> Self {
- self.clone_dispatcher()
- }
-}
-
-impl<C> Program<C>
-where
- C: ProgramCollect<Enum = C>,
-{
- /// Adds a dispatcher to the program.
- #[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,
- {
- #[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_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>>,
- {
- #[cfg(not(feature = "dispatch_tree"))]
- {
- let dispatchers = dispatchers.into();
- self.dispatcher.extend(dispatchers.dispatcher);
- }
- #[cfg(feature = "dispatch_tree")]
- {
- let _ = dispatchers;
- }
- self
- }
-}
-
-/// A collection of dispatchers.
+/// Dispatcher is the first stop for args after they enter the program:
+/// it is used to wrap the user's raw args into an initial [`ChainProcess`] and feed them into the program loop
///
-/// This struct holds a vector of boxed `Dispatcher` trait objects,
-/// allowing multiple dispatchers to be grouped together and passed
-/// to the program via `Program::with_dispatchers`.
-/// A collection of dispatchers.
+/// # Manual impl
///
-/// This struct holds a vector of boxed `Dispatcher` trait objects,
-/// allowing multiple dispatchers to be grouped together and passed
-/// to the program via `Program::with_dispatchers`.
-pub struct Dispatchers<G> {
- dispatcher: Vec<Box<dyn Dispatcher<G> + Send + Sync + 'static>>,
-}
-
-impl<G> From<Vec<Box<dyn Dispatcher<G> + Send + Sync>>> for Dispatchers<G> {
- fn from(dispatcher: Vec<Box<dyn Dispatcher<G> + Send + Sync>>) -> Self {
- Self { dispatcher }
- }
-}
-
-impl<G> From<Box<dyn Dispatcher<G> + Send + Sync>> for Dispatchers<G> {
- fn from(dispatcher: Box<dyn Dispatcher<G> + Send + Sync>) -> Self {
- Self {
- dispatcher: vec![dispatcher],
- }
- }
-}
-
-impl<D, G> From<(D,)> for Dispatchers<G>
-where
- D: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- fn from(dispatcher: (D,)) -> Self {
- Self {
- dispatcher: vec![Box::new(dispatcher.0)],
- }
- }
-}
-
-impl<D1, D2, G> From<(D1, D2)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- fn from(dispatchers: (D1, D2)) -> Self {
- Self {
- dispatcher: vec![Box::new(dispatchers.0), Box::new(dispatchers.1)],
- }
- }
-}
-
-impl<D1, D2, D3, G> From<(D1, D2, D3)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- D3: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- 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, G> From<(D1, D2, D3, D4)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- D3: Dispatcher<G> + Send + Sync + 'static,
- D4: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- 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, G> From<(D1, D2, D3, D4, D5)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- D3: Dispatcher<G> + Send + Sync + 'static,
- D4: Dispatcher<G> + Send + Sync + 'static,
- D5: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- 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, G> From<(D1, D2, D3, D4, D5, D6)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- D3: Dispatcher<G> + Send + Sync + 'static,
- D4: Dispatcher<G> + Send + Sync + 'static,
- D5: Dispatcher<G> + Send + Sync + 'static,
- D6: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- 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, G> From<(D1, D2, D3, D4, D5, D6, D7)> for Dispatchers<G>
-where
- D1: Dispatcher<G> + Send + Sync + 'static,
- D2: Dispatcher<G> + Send + Sync + 'static,
- D3: Dispatcher<G> + Send + Sync + 'static,
- D4: Dispatcher<G> + Send + Sync + 'static,
- D5: Dispatcher<G> + Send + Sync + 'static,
- D6: Dispatcher<G> + Send + Sync + 'static,
- D7: Dispatcher<G> + Send + Sync + 'static,
- G: Display,
-{
- 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<G> std::ops::Deref for Dispatchers<G> {
- type Target = Vec<Box<dyn Dispatcher<G> + Send + Sync + 'static>>;
-
- fn deref(&self) -> &Self::Target {
- &self.dispatcher
- }
-}
-
-impl<G> From<Dispatchers<G>> for Vec<Box<dyn Dispatcher<G> + Send + Sync + 'static>> {
- fn from(val: Dispatchers<G>) -> Self {
- val.dispatcher
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::ChainProcess;
- use std::fmt::Display;
-
- /// A minimal mock Dispatcher for testing Dispatchers conversions.
- #[derive(Clone)]
- struct MockDispatcher {
- name: &'static str,
- }
-
- impl<C: Display> Dispatcher<C> for MockDispatcher {
- fn node(&self) -> crate::asset::node::Node {
- self.name.into()
- }
-
- fn begin(&self, _args: Vec<String>) -> ChainProcess<C> {
- unimplemented!("not used in these tests")
- }
-
- fn clone_dispatcher(&self) -> Box<dyn Dispatcher<C>> {
- Box::new(self.clone())
- }
- }
-
- /// Minimal mock group for Dispatchers tests
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- #[allow(dead_code)]
- enum MockG {
- A,
- }
-
- impl Display for MockG {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "A")
- }
- }
-
- #[test]
- fn test_dispatchers_from_single_tuple() {
- let disp = MockDispatcher { name: "foo" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
- assert_eq!(dispatchers.dispatcher.len(), 1);
- }
-
- #[test]
- fn test_dispatchers_from_two_tuple() {
- let d1 = MockDispatcher { name: "a" };
- let d2 = MockDispatcher { name: "b" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2));
- assert_eq!(dispatchers.dispatcher.len(), 2);
- }
-
- #[test]
- fn test_dispatchers_from_three_tuple() {
- let d1 = MockDispatcher { name: "x" };
- let d2 = MockDispatcher { name: "y" };
- let d3 = MockDispatcher { name: "z" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3));
- assert_eq!(dispatchers.dispatcher.len(), 3);
- }
-
- #[test]
- fn test_dispatchers_from_four_tuple() {
- let d1 = MockDispatcher { name: "1" };
- let d2 = MockDispatcher { name: "2" };
- let d3 = MockDispatcher { name: "3" };
- let d4 = MockDispatcher { name: "4" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4));
- assert_eq!(dispatchers.dispatcher.len(), 4);
- }
-
- #[test]
- fn test_dispatchers_from_five_tuple() {
- let d1 = MockDispatcher { name: "a" };
- let d2 = MockDispatcher { name: "b" };
- let d3 = MockDispatcher { name: "c" };
- let d4 = MockDispatcher { name: "d" };
- let d5 = MockDispatcher { name: "e" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5));
- assert_eq!(dispatchers.dispatcher.len(), 5);
- }
-
- #[test]
- fn test_dispatchers_from_six_tuple() {
- let d1 = MockDispatcher { name: "a" };
- let d2 = MockDispatcher { name: "b" };
- let d3 = MockDispatcher { name: "c" };
- let d4 = MockDispatcher { name: "d" };
- let d5 = MockDispatcher { name: "e" };
- let d6 = MockDispatcher { name: "f" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5, d6));
- assert_eq!(dispatchers.dispatcher.len(), 6);
- }
-
- #[test]
- fn test_dispatchers_from_seven_tuple() {
- let d1 = MockDispatcher { name: "a" };
- let d2 = MockDispatcher { name: "b" };
- let d3 = MockDispatcher { name: "c" };
- let d4 = MockDispatcher { name: "d" };
- let d5 = MockDispatcher { name: "e" };
- let d6 = MockDispatcher { name: "f" };
- let d7 = MockDispatcher { name: "g" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((d1, d2, d3, d4, d5, d6, d7));
- assert_eq!(dispatchers.dispatcher.len(), 7);
- }
-
- #[test]
- fn test_dispatchers_from_vec_of_boxed() {
- let d1: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "a" });
- let d2: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "b" });
- let dispatchers: Dispatchers<MockG> = vec![d1, d2].into();
- assert_eq!(dispatchers.dispatcher.len(), 2);
- }
-
- #[test]
- fn test_dispatchers_from_single_boxed() {
- let d: Box<dyn Dispatcher<MockG> + Send + Sync> = Box::new(MockDispatcher { name: "x" });
- let dispatchers: Dispatchers<MockG> = d.into();
- assert_eq!(dispatchers.dispatcher.len(), 1);
- }
-
- #[test]
- fn test_dispatchers_deref() {
- let disp = MockDispatcher { name: "test" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
- let inner: &Vec<Box<dyn Dispatcher<MockG> + Send + Sync + 'static>> = &dispatchers;
- assert_eq!(inner.len(), 1);
- }
-
- #[test]
- fn test_dispatchers_into_vec() {
- let disp = MockDispatcher { name: "foo" };
- let dispatchers: Dispatchers<MockG> = Dispatchers::from((disp,));
- let vec: Vec<Box<dyn Dispatcher<MockG> + Send + Sync + 'static>> = dispatchers.into();
- assert_eq!(vec.len(), 1);
- }
-
- #[test]
- fn test_box_clone_dispatcher() {
- let disp: Box<dyn Dispatcher<MockG>> = Box::new(MockDispatcher { name: "clonable" });
- let cloned = disp.clone_dispatcher();
- assert_eq!(cloned.node().to_string(), "clonable");
- }
+/// ```
+/// # use mingling_core::ChainProcess;
+/// # use mingling_core::Dispatcher;
+/// # use mingling_core::Grouped;
+/// # use mingling_core::Routable;
+/// # use mingling_core::MockProgramCollect as ThisProgram;
+/// # unsafe impl Grouped<ThisProgram> for Foo {
+/// # fn member_id() -> ThisProgram { ThisProgram::Foo }
+/// # }
+/// struct CMDGreet;
+/// struct Foo {
+/// args: Vec<String>
+/// }
+///
+/// impl Dispatcher<ThisProgram> for CMDGreet {
+/// fn begin(&self, args: Vec<String>) -> ChainProcess<ThisProgram> {
+/// Routable::to_chain(Foo { args })
+/// }
+/// }
+/// ```
+pub trait Dispatcher<C> {
+ /// Begin logic, receives the remaining arguments after the command prefix
+ /// has been stripped
+ ///
+ /// Example:
+ ///
+ /// ```
+ /// # use mingling_core::ChainProcess;
+ /// # use mingling_core::Dispatcher;
+ /// # use mingling_core::Grouped;
+ /// # use mingling_core::Routable;
+ /// # use mingling_core::MockProgramCollect as ThisProgram;
+ /// # unsafe impl Grouped<ThisProgram> for Foo {
+ /// # fn member_id() -> ThisProgram { ThisProgram::Foo }
+ /// # }
+ /// # struct CMDGreet;
+ /// # struct Foo {
+ /// # args: Vec<String>
+ /// # }
+ /// # impl Dispatcher<ThisProgram> for CMDGreet {
+ /// fn begin(&self, args: Vec<String>) -> ChainProcess<ThisProgram> {
+ /// // Create Foo from args and route it to the next chain
+ /// Routable::to_chain(Foo { args })
+ /// }
+ /// # }
+ /// ```
+ fn begin(&self, args: Vec<String>) -> ChainProcess<C>;
}
diff --git a/mingling_core/src/asset/enum_tag.rs b/mingling_core/src/asset/enum_tag.rs
index d830e62..0639e18 100644
--- a/mingling_core/src/asset/enum_tag.rs
+++ b/mingling_core/src/asset/enum_tag.rs
@@ -1,12 +1,53 @@
-/// Marker trait for `EnumTag`
+/// Marks an enum so that its variants can be recognized by Mingling
+///
+/// By implementing [`EnumTag`], Mingling can obtain the enum's name, information, etc.,
+/// which helps with argument parsing, completion, etc.
+///
+/// # Manual impl
+///
+/// In general, [`EnumTag`] is recommended to be derived using [`#[derive(EnumTag)]`](https://docs.rs/mingling/latest/mingling/derive.EnumTag.html),
+/// but if you need to implement it manually, please refer to the following:
+///
+/// ```
+/// # use mingling_core::EnumTag;
+/// enum Choice {
+/// Foo, Bar
+/// }
+///
+/// impl EnumTag for Choice {
+/// fn enum_info(&self) -> (&'static str, &'static str) {
+/// ("Choice", "Choice enum")
+/// }
+/// fn enums() -> &'static [(&'static str, &'static str)] {
+/// &[("Foo", "Foo variant"), ("Bar", "Bar variant")]
+/// }
+/// fn build_enum(name: String) -> Option<Self> {
+/// match name.as_str() {
+/// "Foo" => Some(Choice::Foo),
+/// "Bar" => Some(Choice::Bar),
+/// _ => None,
+/// }
+/// }
+/// }
+/// ```
pub trait EnumTag {
- /// Get the name and description of this enum
+ /// Returns the name and description of the enum
+ ///
+ /// Returns a tuple `(enum_name, enum_description)`, where `enum_name` is the name of the enum,
+ /// and `enum_description` is a brief description of the enum, used in scenarios such as
+ /// argument parsing error messages and completion.
fn enum_info(&self) -> (&'static str, &'static str);
- /// Get all possible enum variant names and descriptions
+ /// Returns the names and descriptions of all variants of this enum
+ ///
+ /// Returns a slice where each element is a `(variant_name, variant_description)` tuple,
+ /// describing all variants of the enum and their meanings, used for argument completion and parsing.
fn enums() -> &'static [(&'static str, &'static str)];
- /// Build the enum from a name
+ /// Builds the corresponding enum value from a string name
+ ///
+ /// The input `name` is the string argument to be parsed. If it matches a variant name,
+ /// the corresponding `Some(enum_value)` is returned; otherwise `None` is returned.
fn build_enum(name: String) -> Option<Self>
where
Self: Sized;
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index 18e4446..27ccfe4 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -6,131 +6,690 @@ use std::{
use crate::{ChainProcess, Program, ProgramCollect, this};
-pub(crate) type GlobalResources = Arc<Mutex<HashMap<TypeId, Box<dyn Any + Sync + Send>>>>;
+/// A standalone, thread-safe container for storing global resources keyed by their type.
+///
+/// This is the resource store behind [`Program`]'s resource API: every `Program`
+/// owns one of these containers and all of its resource operations delegate to it.
+///
+/// Unlike the resource API on [`Program`], this container is **not** coupled to a
+/// program instance nor to the global `this::<C>()` context, so any number of
+/// containers can be created and used at the same time — each with fully
+/// independent storage.
+///
+/// Each resource is stored behind its **own** [`Mutex`]. The container lock is
+/// only held for the brief lookup/clone of the entry, so two nested
+/// `modify_res` calls (e.g. two `&mut` resource parameters generated by
+/// `#[chain]`) lock **different** mutexes and cannot deadlock against each other.
+pub struct GlobalResContainer {
+ /// Thread-safe storage for resources, keyed by their `TypeId` and protected by a `Mutex`.
+ ///
+ /// Each entry is a `Box<dyn Any>` holding an `Arc<Mutex<Arc<Res>>>`: the outer
+ /// `Mutex` guards the entry itself (so a resource can be locked without
+ /// holding the container lock), and the inner `Arc<Res>` is the shared
+ /// immutable snapshot returned by `res()`.
+ map: Mutex<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
+}
-impl<C> Program<C>
-where
- C: ProgramCollect<Enum = C>,
-{
- /// Insert a resource of the given type, cloning the provided value into the store
+impl GlobalResContainer {
+ /// Creates an empty resource container.
+ ///
+ /// Usage:
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let container = GlobalResContainer::new();
+ /// ```
+ #[must_use]
+ pub fn new() -> Self {
+ Self {
+ map: Mutex::new(HashMap::new()),
+ }
+ }
+
+ /// Clones the per-resource entry out from under the container lock.
+ ///
+ /// The container lock is released as soon as the entry `Arc` is cloned,
+ /// so all subsequent operations lock only the resource's own mutex.
+ fn res_entry<Res: 'static>(&self) -> Option<Arc<Mutex<Arc<Res>>>> {
+ let guard = self.map.lock().ok()?;
+ let entry = guard
+ .get(&TypeId::of::<Res>())?
+ .as_ref()
+ .downcast_ref::<Arc<Mutex<Arc<Res>>>>()
+ .map(Arc::clone);
+ drop(guard);
+ entry
+ }
+
+ /// Inserts (or overwrites) a resource into the [`GlobalResContainer`].
+ ///
+ /// # Behavior
+ ///
+ /// - The resource is stored bound to the `TypeId` of its type. That is, **the same type**
+ /// can only have one resource instance — a later inserted resource of the same type will
+ /// **overwrite** the previously inserted old value.
+ /// - Different `Res` types are **completely independent** of each other in the container.
+ /// - Resources are stored as `Arc<Mutex<Arc<Res>>>`: the outer `Mutex` guarantees that
+ /// only one caller can modify the resource at a time (but holding that lock does not
+ /// block the container's global lock); the inner `Arc<Res>` is used to provide immutable
+ /// shared snapshots for read-only APIs such as `res()` / `res_or_default()`.
+ /// - `Res` must satisfy `'static + Send + Sync` (for thread-safe sharing) as well as the
+ /// [`ResourceMarker`] trait (providing default values, cloning, etc.).
+ ///
+ /// # Return Value
+ ///
+ /// Returns `&mut self` for chained calls, for example:
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let mut container = GlobalResContainer::new();
+ /// container
+ /// .with_resource(42i32)
+ /// .with_resource(String::from("hello"));
+ /// ```
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)));
+ if let Ok(mut guard) = self.map.lock() {
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(res)))),
+ );
}
self
}
- /// Modify a resource by type, applying a closure to the resource if present
+ /// Performs a **read-modify-write** operation on an existing resource in the container
+ /// and returns the closure's return value.
+ ///
+ /// # Behavior
+ ///
+ /// 1. First, looks up the resource entry corresponding to the `Res` type in the container.
+ /// 2. If the entry **does not exist** (has not been inserted via [`with_resource`] or
+ /// [`__store_res`]), returns `Return::default()` directly, **without** calling `f`,
+ /// and without inserting any new resource.
+ /// 3. If the entry exists, locks the resource's own mutex (note: this lock is **separate**
+ /// from the container's global lock, so nested calls will not deadlock).
+ /// 4. Takes the resource **out** of the container (attempting to take ownership directly
+ /// via `Arc::try_unwrap`):
+ /// - If the `Arc` has no other holders (i.e., no `GlobalResource` snapshot references it),
+ /// the original value is taken directly, without cloning.
+ /// - If the `Arc` has other holders (e.g., `res()` was called elsewhere and holds a
+ /// shared snapshot), a **clone** is made via `__resource_marker_clone()` for
+ /// modification; the original snapshot is unaffected.
+ /// 5. Passes the cloned/taken value to the closure `f(&mut new_res)` for modification and
+ /// collects the closure's return value `r`.
+ /// 6. Writes the modified new value **back** into the resource slot, then releases the
+ /// resource lock.
+ /// 7. Returns the closure's result `r`.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Default + ResourceMarker + Send + Sync`.
+ /// `ResourceMarker` guarantees the resource can be cloned (when a shared snapshot exists)
+ /// and can be default-instantiated.
+ /// - `Return` must implement `Default`, because when the resource does not exist or the
+ /// lock is poisoned, this method returns `Return::default()` as a fallback value.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(10i32);
+ ///
+ /// // Resource exists: modify and return the value
+ /// let double = container.modify_res(|v: &mut i32| { *v *= 2; *v });
+ /// assert_eq!(double, 20);
+ /// assert_eq!(*container.res::<i32>().unwrap(), 20);
+ ///
+ /// // Resource does not exist: returns Default, closure is not called
+ /// let missing = container.modify_res::<String, i32>(|_| 42);
+ /// assert_eq!(missing, 0);
+ /// ```
+ ///
+ /// [`with_resource`]: Self::with_resource
+ /// [`__store_res`]: Self::__store_res
+ /// [`res()`]: Self::res
pub fn modify_res<Res, Return>(&self, f: impl FnOnce(&mut Res) -> Return) -> Return
where
Res: 'static + Default + ResourceMarker + Send + Sync,
Return: Default,
{
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
return Return::default();
};
- if let Some(arc_res) = guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- {
- let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- };
- let r = f(&mut new_res);
- *arc_res = Arc::new(new_res);
- return r;
- }
- Return::default()
+ let Ok(mut guard) = entry.lock() else {
+ return Return::default();
+ };
+ let mut new_res = match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
+ };
+ let r = f(&mut new_res);
+ *guard = Arc::new(new_res);
+ r
}
- /// Internal syntax for the `&mut MyResource` syntax of #[chain], do not use directly
+ /// Performs a **read-modify-write** operation on an existing resource in the container
+ /// and directly passes the closure's [`ChainProcess<C>`] routing result to the caller.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method used by `#[chain]`, `#[resource]`, and similar macros;
+ /// it typically does not appear directly in user business code.
+ ///
+ /// This method behaves very similarly to [`modify_res`](Self::modify_res), with the
+ /// only differences being:
+ ///
+ /// - The closure `f`'s return type is [`ChainProcess<C>`], rather than an arbitrary generic
+ /// `Return`. This means it is designed for **chained program routing/jumping**
+ /// scenarios — the closure can return `ChainProcess::Ok(...)` /
+ /// `ChainProcess::Err(...)` / jump targets, etc., to route program execution to the
+ /// next stage.
+ /// - When the resource does not exist or the container/resource lock is poisoned,
+ /// `modify_res` returns `Return::default()`, whereas this method constructs a
+ /// **default resource** (via `ResourceMarker::__resource_marker_default()`),
+ /// still calls `f`, and returns the `ChainProcess<C>` produced by `f` directly.
+ /// That is, this method **always** calls the closure `f` and returns its routing result.
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the container.
+ /// - If the entry **does not exist**, constructs a **temporary default resource**
+ /// via `ResourceMarker::__resource_marker_default()`, directly calls
+ /// `f(&mut default_res)`, and returns its result.
+ /// - If the entry exists, continues to step 2.
+ /// 2. Locks the resource's **own** `Mutex` (independent of the container's global lock;
+ /// nested calls will not deadlock).
+ /// - If the lock is poisoned, also goes down the "default resource" branch:
+ /// constructs a default instance and calls `f`.
+ /// 3. Attempts to take the resource's **ownership** via `Arc::try_unwrap`:
+ /// - If the `Arc` has no other holders (no shared snapshots), takes the original value;
+ /// - If there are other holders (e.g., a [`GlobalResource`] snapshot exists elsewhere),
+ /// calls `ResourceMarker::__resource_marker_clone()` to **clone** a copy for
+ /// modification; the original snapshot is unaffected.
+ /// 4. Passes the taken value to the closure `f(&mut new_res)` to execute the modification
+ /// logic, obtaining the routing result `r`.
+ /// 5. Writes the modified new value **back** into the resource slot, releases the
+ /// resource lock.
+ /// 6. Returns `r` to the caller.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Default + ResourceMarker + Send + Sync`.
+ /// `ResourceMarker` provides cloning and default instantiation capabilities;
+ /// `Default` is used to construct fallback values.
+ /// - `C` must implement `ProgramCollect<Enum = C>`, i.e., the current program's
+ /// collector type.
+ /// - `ChainProcess<C>` represents an intermediate/terminal state of chained program
+ /// execution, decoded by macro-expanded code to determine the next step.
+ ///
+ /// # Role as a Macro-Expansion Internal Method
+ ///
+ /// In code generated by `#[chain]` and similar macros, when a resource needs to be
+ /// injected as a **mutable reference** (`&mut Res`) into a procedure/step, while also
+ /// ensuring that the procedure can return a `ChainProcess<C>` to drive program-flow
+ /// routing, the macro expansion generates a call to `__modify_res_and_return_route`.
+ /// This ensures:
+ ///
+ /// - The resource modification and the `ChainProcess` routing result are completed in
+ /// **one atomic operation**;
+ /// - Regardless of whether the resource exists, macro-expanded code can obtain a
+ /// `ChainProcess<C>` to continue program execution flow;
+ /// - Macro-generated code does not need to worry about internal locking, `Arc`
+ /// ownership, or cloning details, all encapsulated by this method.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{GlobalResContainer, ChainProcess, error::ChainProcessError};
+ /// # use mingling_core::MockProgramCollect;
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(1i32);
+ ///
+ /// // After macro expansion, this is equivalent to: take out the i32 resource,
+ /// // modify it, and return the routing result
+ /// let route: ChainProcess<MockProgramCollect> =
+ /// container.__modify_res_and_return_route(|v: &mut i32| {
+ /// *v += 1;
+ /// ChainProcess::Err(ChainProcessError::Other("done".into()))
+ /// });
+ /// assert!(matches!(route, ChainProcess::Err(_)));
+ /// assert_eq!(*container.res::<i32>().unwrap(), 2);
+ /// ```
+ ///
+ /// [`ChainProcess<C>`]: crate::ChainProcess
+ /// [`Program::__modify_res_and_return_route`]: crate::Program::__modify_res_and_return_route
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`**, and the API will not be publicly exposed in
+ /// stable documentation. Do not call it directly in business code; use public macros or
+ /// the public [`Program`] methods to operate on resources.
#[doc(hidden)]
- pub fn __modify_res_and_return_route<Res>(
+ pub fn __modify_res_and_return_route<Res, C>(
&self,
f: impl FnOnce(&mut Res) -> ChainProcess<C>,
) -> ChainProcess<C>
where
Res: 'static + Default + ResourceMarker + Send + Sync,
+ C: ProgramCollect<Enum = C>,
{
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
let mut default_res = Res::__resource_marker_default();
return f(&mut default_res);
};
- if let Some(arc_res) = guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- {
- let mut new_res = match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- };
- let r = f(&mut new_res);
- *arc_res = Arc::new(new_res);
- r
- } else {
+ let Ok(mut guard) = entry.lock() else {
let mut default_res = Res::__resource_marker_default();
- f(&mut default_res)
- }
+ return f(&mut default_res);
+ };
+ let mut new_res = match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
+ };
+ let r = f(&mut new_res);
+ *guard = Arc::new(new_res);
+ r
}
- /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ /// **Takes** a `Res`-typed resource value **out** of the container and returns its
+ /// ownership.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method used by `#[chain]`, `#[resource]`, and similar macros;
+ /// it typically does not appear directly in user business code.
+ ///
+ /// This method differs from the public [`modify_res`](Self::modify_res):
+ ///
+ /// - `modify_res` is a **modify-and-write-back** operation, where the value modified by
+ /// the closure is **written back** into the container's resource slot;
+ /// - This method performs a **take-and-return** operation — it requires no closure and
+ /// does not write the modified value back into the container. It **moves** the resource
+ /// **out of** the container into the caller's hands, granting the caller full ownership
+ /// (or an independent copy) of the resource, so it can be used independently of the
+ /// container.
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the container:
+ /// - If the entry **does not exist**, constructs and returns a default instance via
+ /// `ResourceMarker::__resource_marker_default()`.
+ /// - If the entry exists, continues to step 2.
+ /// 2. Locks the resource's **own** `Mutex`. If the lock is poisoned, also returns a
+ /// default instance.
+ /// 3. Attempts to take the resource's **ownership** via `Arc::try_unwrap`:
+ /// - If the `Arc` has no other holders (no shared snapshots), returns the original
+ /// value directly;
+ /// - If there are other holders (e.g., a [`GlobalResource`] shared snapshot exists),
+ /// calls `ResourceMarker::__resource_marker_clone()` to **clone** a copy and returns
+ /// it; the original value in the container is unaffected (not written back).
+ /// 4. Returns the taken `Res` value.
+ ///
+ /// # Resource Slot State
+ ///
+ /// This method does **not** write a new value back into the container, so the resource
+ /// slot is **cleared** after being taken (since it internally uses `mem::take`, the
+ /// slot is emptied). Subsequent calls to [`res()`](Self::res) /
+ /// [`modify_res`](Self::modify_res) for the same resource will find that the entry
+ /// **still exists** (the `TypeId` key is still present), but the `Arc` inside the slot
+ /// has been emptied and replaced with a `Default::default()`-typed empty value — the
+ /// exact behavior depends on internal implementation details, and macro-expanded code
+ /// should not rely on the precise state of the slot after extraction.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Default + ResourceMarker + Send + Sync`.
+ ///
+ /// # Role as a Macro-Expansion Internal Method
+ ///
+ /// In macro-generated code, when a resource needs to be **taken out at once** from the
+ /// container and handed to an operation that requires ownership (rather than borrowing)
+ /// — e.g., moving the resource to another container, passing it to an
+ /// `impl FnOnce(Res)`-style closure, or participating in special `Arc::try_unwrap`
+ /// semantics — the macro expansion calls this method. It avoids borrow-lifetime
+ /// entanglement and delivers ownership of the value directly.
+ ///
+ /// # Example (simulating macro-expansion internal calls)
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(3i32);
+ ///
+ /// let extracted: i32 = container.__extract_res_mut();
+ /// assert_eq!(extracted, 3);
+ /// ```
///
- /// Extracts a mutable resource from the global store (clone-out), returning an
- /// owned value. The caller must call [`__store_res`] to write back modifications.
+ /// [`Program::__extract_res_mut`]: crate::Program::__extract_res_mut
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`**, and the API will not be publicly exposed in
+ /// stable documentation. Do not call it directly in business code.
#[doc(hidden)]
#[must_use]
pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res {
- let Ok(mut guard) = self.resources.lock() else {
+ let Some(entry) = self.res_entry::<Res>() else {
return Res::__resource_marker_default();
};
- if let Some(arc_res) = guard
- .get_mut(&TypeId::of::<Res>())
- .and_then(|a| a.downcast_mut::<Arc<Res>>())
- {
- match Arc::try_unwrap(std::mem::take(arc_res)) {
- Ok(val) => val,
- Err(arc) => (*arc).__resource_marker_clone(),
- }
- } else {
- Res::__resource_marker_default()
+ let Ok(mut guard) = entry.lock() else {
+ return Res::__resource_marker_default();
+ };
+ match Arc::try_unwrap(std::mem::take(&mut *guard)) {
+ Ok(val) => val,
+ Err(arc) => (*arc).__resource_marker_clone(),
}
}
- /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly.
+ /// **Overwrites** a resource value into the container.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method used by `#[chain]`, `#[resource]`, and similar macros;
+ /// it typically does not appear directly in user business code.
+ ///
+ /// This method behaves **almost identically** to the public
+ /// [`with_resource`](Self::with_resource) (both store/overwrite a resource by type), but
+ /// the two differ in their method signatures:
+ ///
+ /// - [`with_resource`](Self::with_resource) takes `&mut self` and returns `&mut Self`,
+ /// suitable for chained **initialization** scenarios (e.g., registering multiple
+ /// resources at once during `Program` construction);
+ /// - `__store_res` takes `&self` and returns no value, suitable for **runtime**
+ /// dynamic overwrite/update of a resource's value (e.g., macro-expanded code already
+ /// holding an immutable reference).
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Acquires the container's global lock. If the lock is poisoned, returns directly
+ /// without doing anything.
+ /// 2. Looks up the existing entry by `Res` type in the container:
+ /// - If the entry **does not exist**, creates a new `Arc<Mutex<Arc<Res>>>` wrapper
+ /// under the `TypeId::of::<Res>()` key and inserts it into the container.
+ /// - If the entry **already exists**, attempts to downcast `boxed_any` to
+ /// `Arc<Mutex<Arc<Res>>>` and attempts to lock the resource's own `Mutex`:
+ /// * If downcasting succeeds and locking succeeds, writes the new `Arc::new(val)`
+ /// directly into that slot, completing the overwrite update;
+ /// * If downcasting fails (which should not happen in theory, since the type is
+ /// guaranteed by `TypeId`) or the lock is poisoned, **replaces the entire entry**
+ /// (constructs a new `Arc<Mutex<Arc<Res>>>` and inserts it).
+ /// 3. Releases the container's global lock; the operation is complete.
+ ///
+ /// # Concurrency Semantics
///
- /// Stores a modified resource value back into the global store.
+ /// - The container's global lock and the resource's own lock are **two independent
+ /// locks**. This method briefly holds the container's global lock to locate the
+ /// entry, then writes the new value **only** under the protection of the resource's
+ /// own lock — other threads calling `res()` / `modify_res()` for the same resource
+ /// will not see intermediate states.
+ /// - If the container's global lock is poisoned, this method silently returns; if the
+ /// resource's own lock is poisoned, this method **replaces the entire entry** (discard
+ /// the old lock state), ensuring the new value can still be written successfully.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Send + Sync + ResourceMarker`.
+ ///
+ /// # Role as a Macro-Expansion Internal Method
+ ///
+ /// In macro-generated code, when a resource of a certain type needs to be **overwritten
+ /// at runtime** (e.g., writing a `&mut Res` parameter back into the container after a
+ /// procedure ends, or committing a newly computed resource snapshot back to the
+ /// container), the macro expansion calls this method. It allows updating a resource's
+ /// value in an environment where only `&self` (an immutable reference) is available,
+ /// decoupling the resource's lifetime from the caller's borrow of the container.
+ ///
+ /// # Example (simulating macro-expansion internal calls)
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let container = GlobalResContainer::new();
+ ///
+ /// // Runtime write
+ /// container.__store_res(8i32);
+ /// assert_eq!(*container.res::<i32>().unwrap(), 8);
+ ///
+ /// // Overwrite
+ /// container.__store_res(9i32);
+ /// assert_eq!(*container.res::<i32>().unwrap(), 9);
+ /// ```
+ ///
+ /// [`Program::__store_res`]: crate::Program::__store_res
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`**, and the API will not be publicly exposed in
+ /// stable documentation. Do not call it directly in business code; use the public
+ /// [`with_resource`](Self::with_resource) or [`modify_res`](Self::modify_res) public
+ /// methods instead.
#[doc(hidden)]
pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(&self, val: Res) {
- if let Ok(mut guard) = self.resources.lock() {
- guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(val)));
+ let Ok(mut guard) = self.map.lock() else {
+ return;
+ };
+ let Some(boxed_any) = guard.get_mut(&TypeId::of::<Res>()) else {
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(val)))),
+ );
+ return;
+ };
+ if let Some(entry) = boxed_any.downcast_mut::<Arc<Mutex<Arc<Res>>>>()
+ && let Ok(mut entry_guard) = entry.lock()
+ {
+ *entry_guard = Arc::new(val);
+ return;
}
+ // The entry exists but cannot be updated (type mismatch or poisoned
+ // lock): replace it wholesale.
+ guard.insert(
+ TypeId::of::<Res>(),
+ Box::new(Arc::new(Mutex::new(Arc::new(val)))),
+ );
}
- /// Get an resources by type, returning `Res` if present
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the container.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the container:
+ /// - If the entry **does not exist** (has not been inserted via
+ /// [`with_resource`](Self::with_resource) or [`__store_res`](Self::__store_res)),
+ /// returns `None`.
+ /// - If the entry exists, continues to step 2.
+ /// 2. Locks the resource's **own** `Mutex` (independent of the container's global lock;
+ /// nested calls will not deadlock).
+ /// 3. If locking succeeds, clones the resource's internal `Arc<Res>` and wraps it as a
+ /// [`GlobalResource<Res>`] to return.
+ ///
+ /// The returned [`GlobalResource<Res>`] can be dereferenced like `&Res` via `Deref`.
+ /// When multiple callers each hold their own returned [`GlobalResource<Res>`], they share
+ /// the same underlying `Arc<Res>` data, maintaining **read-only consistency** with each
+ /// other.
+ ///
+ /// # Relationship with Modification Operations
+ ///
+ /// - This method returns an **immutable snapshot** (`Arc<Res>`) of the resource and does
+ /// not block or wait for other threads to modify the resource (modification operations
+ /// lock the resource's own `Mutex`).
+ /// - When a [`GlobalResource<Res>`] snapshot is held externally (`Arc` strong reference
+ /// count > 1), subsequent [`modify_res`](Self::modify_res) operations on the same
+ /// resource will **clone** a copy for modification, without affecting the snapshot
+ /// obtained here.
+ ///
+ /// # Return Value
+ ///
+ /// Returns `Option<GlobalResource<Res>>`:
+ /// - If the resource exists and the lock can be acquired normally, returns
+ /// `Some(GlobalResource<Res>)`;
+ /// - If the resource does not exist or the resource lock is poisoned, returns `None`.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Send + Sync`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(10i32);
+ ///
+ /// let res = container.res::<i32>();
+ /// assert_eq!(*res.unwrap(), 10);
+ ///
+ /// // Resource does not exist: returns None
+ /// assert!(container.res::<String>().is_none());
+ /// ```
+ ///
+ /// If you want a default value when the entry does not exist, use
+ /// [`res_or_default`](Self::res_or_default); if you need to route to a specific path
+ /// when the resource is missing, use [`res_or_route`](Self::res_or_route).
#[must_use]
pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> {
- let guard = self.resources.lock().ok()?;
- let boxed_any = guard.get(&TypeId::of::<Res>())?;
- let arc_res = boxed_any.as_ref().downcast_ref::<Arc<Res>>()?;
- Some(GlobalResource::from(Arc::clone(arc_res)))
+ let entry = self.res_entry::<Res>()?;
+ let guard = entry.lock().ok()?;
+ Some(GlobalResource::from(Arc::clone(&*guard)))
}
- /// Get a resource by type, returning `GlobalResource<Res>` if present
- pub fn res_or_route<Res: 'static + Send + Sync>(
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the container;
+ /// returns the provided routing result if the resource does not exist.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the container:
+ /// - If the entry **exists** and the resource lock can be acquired normally, clones
+ /// the resource's internal `Arc<Res>` (wrapped via [`GlobalResource`]) and returns
+ /// it as `Ok(...)`.
+ /// - If the entry **does not exist** (has not been inserted via
+ /// [`with_resource`](Self::with_resource) or [`__store_res`](Self::__store_res)),
+ /// returns `Err(route)` directly — the caller-provided `ChainProcess<C>` is
+ /// returned as-is, without calling any closure.
+ /// 2. A poisoned lock is treated the same as a missing resource: returns `Err(route)`.
+ ///
+ /// # Return Value
+ ///
+ /// Returns `Result<GlobalResource<Res>, ChainProcess<C>>`:
+ /// - When the resource exists, returns `Ok(GlobalResource<Res>)` (a shared immutable
+ /// snapshot);
+ /// - When the resource does not exist or the resource lock is poisoned, returns
+ /// `Err(route)` (the provided routing result is returned as-is).
+ ///
+ /// # Purpose
+ ///
+ /// In chained program-routing (`#[chain]` and similar macros) scenarios, when a resource
+ /// is missing, it is usually desirable to route the program flow to some error-handling
+ /// branch or default path. This method allows the caller to pre-construct a
+ /// [`ChainProcess<C>`] route: when the resource exists, processing continues normally;
+ /// when the resource is missing, routing jumps to that path in place, avoiding
+ /// additional nested checks.
+ ///
+ /// # Errors
+ ///
+ /// Returns `Err(route)` (the provided `ChainProcess<C>` is returned as-is) when:
+ /// - The resource entry does not exist in the container;
+ /// - The resource's own lock is poisoned.
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Send + Sync`.
+ /// - `C` must implement `ProgramCollect<Enum = C>`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{GlobalResContainer, ChainProcess, error::ChainProcessError};
+ /// # use mingling_core::MockProgramCollect;
+ /// let container = GlobalResContainer::new();
+ /// let route: ChainProcess<MockProgramCollect> =
+ /// ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ ///
+ /// // Resource missing: returns Err(route)
+ /// // Note: ChainProcess is not Clone, so pass each route by value.
+ /// assert!(container.res_or_route::<i32, MockProgramCollect>(route).is_err());
+ ///
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(42i32);
+ /// // Resource exists: returns Ok(GlobalResource)
+ /// let route: ChainProcess<MockProgramCollect> =
+ /// ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ /// match container.res_or_route::<i32, MockProgramCollect>(route) {
+ /// Ok(res) => assert_eq!(*res, 42),
+ /// Err(_) => panic!("expected Ok"),
+ /// }
+ /// ```
+ ///
+ /// [`ChainProcess<C>`]: crate::ChainProcess
+ /// [`GlobalResource`]: crate::GlobalResource
+ pub fn res_or_route<Res, C>(
&self,
route: ChainProcess<C>,
- ) -> Result<GlobalResource<Res>, ChainProcess<C>> {
- match self.res() {
- Some(r) => Ok(r),
- None => Err(route),
- }
+ ) -> Result<GlobalResource<Res>, ChainProcess<C>>
+ where
+ Res: 'static + Send + Sync,
+ C: ProgramCollect<Enum = C>,
+ {
+ self.res().map_or_else(|| Err(route), Ok)
}
- /// Get a resource by type, returning `GlobalResource<Res>` or inserting a default
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the container;
+ /// returns a default instance if the resource does not exist.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the container:
+ /// - If the entry **exists** and the resource lock can be acquired normally, clones
+ /// the resource's internal `Arc<Res>`, wraps it as a [`GlobalResource<Res>`], and
+ /// returns it.
+ /// - If the entry **does not exist** (has not been inserted via
+ /// [`with_resource`](Self::with_resource) or [`__store_res`](Self::__store_res)),
+ /// constructs a **default instance** via
+ /// `ResourceMarker::__resource_marker_default()`, wraps it as a
+ /// [`GlobalResource<Res>`], and returns it.
+ /// 2. A poisoned lock is treated the same as a missing resource: it also returns a
+ /// default instance.
+ ///
+ /// The returned [`GlobalResource<Res>`] can be dereferenced like `&Res` via `Deref`.
+ /// When multiple callers each hold their own returned [`GlobalResource<Res>`], they share
+ /// the same underlying `Arc<Res>` data, maintaining **read-only consistency** with each
+ /// other.
+ ///
+ /// # Difference from [`res()`](Self::res)
+ ///
+ /// - [`res()`](Self::res) returns `None` when the resource is missing or the lock is
+ /// poisoned, requiring the caller to handle the `Option` themselves;
+ /// - This method returns a default value constructed by
+ /// `ResourceMarker::__resource_marker_default()` in the same scenario, so the caller
+ /// does not need to handle the missing branch and can use the return value directly.
+ ///
+ /// If you need to route to a specific path (rather than return a default value) when the
+ /// resource is missing, use [`res_or_route`](Self::res_or_route).
+ ///
+ /// # Constraints
+ ///
+ /// - `Res` must satisfy `'static + Send + Sync + ResourceMarker` (`ResourceMarker`
+ /// provides default instantiation capability).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResContainer;
+ /// let mut container = GlobalResContainer::new();
+ /// container.with_resource(10i32);
+ ///
+ /// // Resource exists: returns the actual value
+ /// assert_eq!(*container.res_or_default::<i32>(), 10);
+ ///
+ /// // Resource does not exist: returns the default value (i32::default() == 0)
+ /// assert_eq!(*container.res_or_default::<String>(), "");
+ /// ```
+ ///
+ /// [`GlobalResource<Res>`]: crate::GlobalResource
+ /// [`with_resource`]: Self::with_resource
+ /// [`__store_res`]: Self::__store_res
#[must_use]
pub fn res_or_default<Res: 'static + Send + Sync + ResourceMarker>(
&self,
@@ -140,13 +699,547 @@ where
}
}
-/// Global assets for storing Program global state information
+impl Default for GlobalResContainer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<C> Program<C>
+where
+ C: ProgramCollect<Enum = C>,
+{
+ /// Inserts (or overwrites) a resource into the program's global resource container.
+ ///
+ /// This is a convenience wrapper around [`GlobalResContainer::with_resource`] that
+ /// delegates to the `Program`'s internal `resources` container. The resource is stored
+ /// keyed by its type with the same semantics as [`GlobalResContainer::with_resource`]:
+ /// the same `Res` type can only hold one instance, and inserting the same type twice
+ /// overwrites the previous value.
+ ///
+ /// # Parameters
+ ///
+ /// - `res`: The resource value to store. It must satisfy:
+ /// - `'static` — the value cannot contain borrowed data tied to a temporary lifetime.
+ /// - `Send + Sync` — it must be safe to share across threads.
+ /// - [`ResourceMarker`] — providing default value, clone, and modify capabilities.
+ ///
+ /// # Return Value
+ ///
+ /// Returns `&mut self` to allow method chaining, e.g.:
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program
+ /// .with_resource(42i32)
+ /// .with_resource(String::from("hello"));
+ /// ```
+ ///
+ /// # See Also
+ ///
+ /// - [`Self::res`] — read a shared immutable snapshot of a resource.
+ /// - [`Self::modify_res`] — read-modify-write a resource in place.
+ /// - [`GlobalResContainer::with_resource`] — the underlying implementation.
+ pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(
+ &mut self,
+ res: Res,
+ ) -> &mut Self {
+ self.resources.with_resource(res);
+ self
+ }
+
+ /// Performs a **read-modify-write** operation on a resource stored in the program's
+ /// resource container and returns the closure's return value.
+ ///
+ /// This method delegates to [`GlobalResContainer::modify_res`], which provides the
+ /// full semantics of the read-modify-write operation. In summary:
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the program's resource container.
+ /// 2. If the entry does **not** exist (has not been inserted via [`with_resource`] or
+ /// [`__store_res`]), returns `Return::default()` immediately **without** calling `f`.
+ /// 3. If the entry exists, locks the resource's **own** mutex (distinct from the
+ /// container's global lock, so nested `modify_res` calls do not deadlock).
+ /// 4. Attempts to take ownership of the value via `Arc::try_unwrap`:
+ /// - If no other shared snapshot exists, the original value is taken directly.
+ /// - If another `GlobalResource` holds a snapshot, a **clone** is made via
+ /// `ResourceMarker::__resource_marker_clone()` for modification.
+ /// 5. Calls the closure `f(&mut res)` with the available (`&mut`) reference.
+ /// 6. Writes the modified value back into the resource slot and releases the lock.
+ /// 7. Returns the closure's return value `r`.
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type to modify. Must satisfy
+ /// `'static + Default + ResourceMarker + Send + Sync`.
+ /// - `Return`: The closure's return type. Must implement [`Default`] since a fallback
+ /// value is returned when the resource does not exist.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(10i32);
+ ///
+ /// let doubled = program.modify_res(|v: &mut i32| { *v *= 2; *v });
+ /// assert_eq!(doubled, 20);
+ /// assert_eq!(*program.res::<i32>().unwrap(), 20);
+ ///
+ /// // Resource does not exist: returns Default without invoking the closure.
+ /// let missing = program.modify_res::<String, i32>(|_| 42);
+ /// assert_eq!(missing, 0);
+ /// ```
+ ///
+ /// [`with_resource`]: Self::with_resource
+ /// [`__store_res`]: Self::__store_res
+ pub fn modify_res<Res, Return>(&self, f: impl FnOnce(&mut Res) -> Return) -> Return
+ where
+ Res: 'static + Default + ResourceMarker + Send + Sync,
+ Return: Default,
+ {
+ self.resources.modify_res(f)
+ }
+
+ /// Performs a **read-modify-write** operation on a resource and returns a
+ /// [`ChainProcess<C>`] routing result.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method primarily used by `#[chain]` and related macros. It
+ /// behaves similarly to [`modify_res`](Self::modify_res) but is designed for
+ /// **chained program-routing** scenarios: the closure returns a [`ChainProcess<C>`]
+ /// that drives program execution to the next stage. Unlike `modify_res`, this method
+ /// **always** invokes the closure `f`, even when the resource does not exist or the
+ /// lock is poisoned (in those cases it constructs a temporary default resource via
+ /// `ResourceMarker::__resource_marker_default()`).
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Looks up the resource entry by `Res` type in the program's resource container.
+ /// - If missing, constructs a **default resource**, calls `f(&mut def)`, and returns
+ /// its [`ChainProcess<C>`] result.
+ /// - If present, continues to step 2.
+ /// 2. Locks the resource's **own** mutex (independent of the container's global lock,
+ /// so nested calls do not deadlock). If the lock is poisoned, also goes down the
+ /// default-resource branch above.
+ /// 3. Attempts to take ownership via `Arc::try_unwrap`; otherwise clones a copy for
+ /// modification using `ResourceMarker::__resource_marker_clone()`.
+ /// 4. Calls `f(&mut new_res)`, obtains the routing result `r`, and writes the modified
+ /// value back into the resource slot.
+ /// 5. Returns `r`.
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type. Must satisfy
+ /// `'static + Default + ResourceMarker + Send + Sync`.
+ /// - `C`: The program collector type, constrained by the outer `impl` block to
+ /// `ProgramCollect<Enum = C>`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{Program, ChainProcess, error::ChainProcessError};
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(1i32);
+ ///
+ /// let route: ChainProcess<ThisProgram> =
+ /// program.__modify_res_and_return_route(|v: &mut i32| {
+ /// *v += 1;
+ /// ChainProcess::Err(ChainProcessError::Other("done".into()))
+ /// });
+ /// assert!(matches!(route, ChainProcess::Err(_)));
+ /// assert_eq!(*program.res::<i32>().unwrap(), 2);
+ /// ```
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`** and is not intended for direct use in
+ /// business code. Use public macro-generated code or public APIs (e.g.
+ /// [`modify_res`](Self::modify_res)) instead.
+ #[doc(hidden)]
+ pub fn __modify_res_and_return_route<Res>(
+ &self,
+ f: impl FnOnce(&mut Res) -> ChainProcess<C>,
+ ) -> ChainProcess<C>
+ where
+ Res: 'static + Default + ResourceMarker + Send + Sync,
+ {
+ self.resources.__modify_res_and_return_route(f)
+ }
+
+ /// **Takes** a `Res`-typed resource value **out** of the program's resource container
+ /// and returns its ownership.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method used by `#[chain]`, `#[resource]`, and similar macros.
+ /// Unlike [`modify_res`](Self::modify_res), which modifies a resource in place and
+ /// writes it back, this method **moves** the resource out of the container (or clones
+ /// an independent copy when shared snapshots exist) and returns it to the caller.
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Looks up the resource entry by `Res` type.
+ /// - If missing, constructs and returns a **default instance** via
+ /// `ResourceMarker::__resource_marker_default()`.
+ /// 2. Locks the resource's own mutex. If poisoned, also returns a default instance.
+ /// 3. Tries `Arc::try_unwrap` to move the original value out; otherwise clones a copy
+ /// via `ResourceMarker::__resource_marker_clone()`.
+ /// 4. Returns the taken `Res` value. Note that the resource slot is **cleared** by
+ /// this operation; subsequent calls to [`res`](Self::res) for the same resource may
+ /// observe an emptied/default-valued slot.
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type to extract. Must satisfy
+ /// `'static + Default + ResourceMarker + Send + Sync`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(3i32);
+ /// let extracted: i32 = program.__extract_res_mut();
+ /// assert_eq!(extracted, 3);
+ /// ```
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`** and is not intended for direct use in
+ /// business code.
+ #[doc(hidden)]
+ #[must_use]
+ pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res {
+ self.resources.__extract_res_mut()
+ }
+
+ /// **Overwrites** a resource value into the program's resource container.
+ ///
+ /// # Purpose
+ ///
+ /// This is an internal method used by `#[chain]`, `#[resource]`, and similar macros.
+ /// It behaves almost identically to [`with_resource`](Self::with_resource) (both
+ /// store/overwrite a resource by type), but differs in signature:
+ ///
+ /// - [`with_resource`](Self::with_resource) takes `&mut self` and returns `&mut Self`,
+ /// suitable for chained initialization.
+ /// - `__store_res` takes `&self` and returns no value, suitable for **runtime**
+ /// dynamic overwrite/update when only an immutable reference is available.
+ ///
+ /// # Execution Steps
+ ///
+ /// 1. Acquires the container's global lock (silently returns if poisoned).
+ /// 2. Looks up the existing entry by `Res` type:
+ /// - If the entry does **not** exist, creates a new `Arc<Mutex<Arc<Res>>>` wrapper
+ /// under `TypeId::of::<Res>()` and inserts it.
+ /// - If it already exists, attempts to update it in place under the resource's own
+ /// lock. If that fails (type mismatch or poisoned lock), replaces the entire entry.
+ /// 3. Releases the container's global lock.
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type. Must satisfy
+ /// `'static + Send + Sync + ResourceMarker`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ ///
+ /// // Runtime write
+ /// program.__store_res(8i32);
+ /// assert_eq!(*program.res::<i32>().unwrap(), 8);
+ ///
+ /// // Overwrite
+ /// program.__store_res(9i32);
+ /// assert_eq!(*program.res::<i32>().unwrap(), 9);
+ /// ```
+ ///
+ /// # Note
+ ///
+ /// This method is **`#[doc(hidden)]`** and is not intended for direct use in
+ /// business code. Use [`with_resource`](Self::with_resource) or
+ /// [`modify_res`](Self::modify_res) instead.
+ #[doc(hidden)]
+ pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(&self, val: Res) {
+ self.resources.__store_res(val);
+ }
+
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the
+ /// program's resource container.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type:
+ /// - If the entry **does not exist** (has not been inserted via
+ /// [`with_resource`](Self::with_resource) or [`__store_res`](Self::__store_res)),
+ /// returns `None`.
+ /// - If the entry exists, continues to step 2.
+ /// 2. Locks the resource's **own** mutex (independent of the container's global lock,
+ /// so nested calls do not deadlock).
+ /// 3. On success, clones the internal `Arc<Res>` and wraps it as a
+ /// [`GlobalResource<Res>`] to return.
+ ///
+ /// The returned [`GlobalResource<Res>`] can be dereferenced like `&Res` via `Deref`.
+ /// Multiple callers holding their own [`GlobalResource<Res>`] share the same underlying
+ /// `Arc<Res>` data, providing **read-only consistency**.
+ ///
+ /// # Return Value
+ ///
+ /// Returns `Option<GlobalResource<Res>>`:
+ /// - `Some(GlobalResource<Res>)` when the resource exists and the lock can be acquired;
+ /// - `None` when the resource does not exist or its lock is poisoned.
+ ///
+ /// If you want a default value in the missing case, use [`res_or_default`](Self::res_or_default);
+ /// if you need to route to a specific path, use [`res_or_route`](Self::res_or_route).
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type to read. Must satisfy `'static + Send + Sync`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(10i32);
+ ///
+ /// let res = program.res::<i32>();
+ /// assert_eq!(*res.unwrap(), 10);
+ ///
+ /// // Resource does not exist: returns None
+ /// assert!(program.res::<String>().is_none());
+ /// ```
+ #[must_use]
+ pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> {
+ self.resources.res()
+ }
+
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the
+ /// program's resource container; returns the provided routing result if the resource
+ /// does not exist.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type:
+ /// - If the entry **exists** and the resource lock can be acquired, clones the
+ /// internal `Arc<Res>` and returns it as `Ok(GlobalResource<Res>)`.
+ /// - If the entry **does not exist** (or its lock is poisoned), returns
+ /// `Err(route)` — the caller-provided [`ChainProcess<C>`] is returned as-is,
+ /// without calling any closure.
+ ///
+ /// # Purpose
+ ///
+ /// In chained program-routing scenarios (e.g. `#[chain]`), when a resource is missing
+ /// it is desirable to route program flow to an error-handling branch or default path.
+ /// This method allows the caller to pre-construct a [`ChainProcess<C>`] route so that
+ /// missing-resource handling is encapsulated in a single call.
+ ///
+ /// # Errors
+ ///
+ /// Returns `Err(route)` (the provided [`ChainProcess<C>`] as-is) when:
+ /// - The resource entry does not exist in the container.
+ /// - The resource's own lock is poisoned.
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type to read. Must satisfy `'static + Send + Sync`.
+ /// - `C`: The program collector type, constrained by the outer `impl` block to
+ /// `ProgramCollect<Enum = C>`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{Program, ChainProcess, error::ChainProcessError};
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// let route: ChainProcess<ThisProgram> =
+ /// ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ ///
+ /// // Resource missing: returns Err(route)
+ /// // Note: ChainProcess is not Clone, so pass the route by value.
+ /// assert!(program.res_or_route::<i32>(route).is_err());
+ ///
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(42i32);
+ /// // Resource exists: returns Ok(GlobalResource)
+ /// let route: ChainProcess<ThisProgram> =
+ /// ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ /// match program.res_or_route::<i32>(route) {
+ /// Ok(res) => assert_eq!(*res, 42),
+ /// Err(_) => panic!("expected Ok"),
+ /// }
+ /// ```
+ pub fn res_or_route<Res: 'static + Send + Sync>(
+ &self,
+ route: ChainProcess<C>,
+ ) -> Result<GlobalResource<Res>, ChainProcess<C>> {
+ self.resources.res_or_route(route)
+ }
+
+ /// Retrieves an **immutable shared snapshot** of a `Res`-typed resource from the
+ /// program's resource container; returns a default instance if the resource does not
+ /// exist.
+ ///
+ /// # Behavior
+ ///
+ /// 1. Looks up the resource entry by `Res` type:
+ /// - If the entry **exists** and the resource lock can be acquired, clones the
+ /// internal `Arc<Res>`, wraps it as a [`GlobalResource<Res>`], and returns it.
+ /// - If the entry **does not exist** (or its lock is poisoned), constructs a
+ /// default instance via `ResourceMarker::__resource_marker_default()`, wraps it
+ /// as a [`GlobalResource<Res>`], and returns it.
+ ///
+ /// # Difference from [`res`](Self::res)
+ ///
+ /// - [`res`](Self::res) returns `None` when the resource is missing or the lock is
+ /// poisoned, requiring the caller to handle the `Option` themselves.
+ /// - This method returns a default value in the same scenario, so the caller does not
+ /// need to handle the missing branch.
+ ///
+ /// If you need to route to a specific path (rather than return a default value) when
+ /// the resource is missing, use [`res_or_route`](Self::res_or_route).
+ ///
+ /// # Type Parameters
+ ///
+ /// - `Res`: The resource type to read. Must satisfy
+ /// `'static + Send + Sync + ResourceMarker` (the latter provides default
+ /// instantiation capability).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::Program;
+ /// use mingling_core::MockProgramCollect as ThisProgram;
+ /// let mut program = Program::<ThisProgram>::new_with_args(Vec::<String>::new());
+ /// program.with_resource(10i32);
+ ///
+ /// // Resource exists: returns the actual value
+ /// assert_eq!(*program.res_or_default::<i32>(), 10);
+ ///
+ /// // Resource does not exist: returns the default value (i32::default() == 0)
+ /// assert_eq!(*program.res_or_default::<String>(), "");
+ /// ```
+ #[must_use]
+ pub fn res_or_default<Res: 'static + Send + Sync + ResourceMarker>(
+ &self,
+ ) -> GlobalResource<Res> {
+ self.resources.res_or_default()
+ }
+}
+
+/// Global type wrapper.
+///
+/// `GlobalResource` is a **thread-safe shared immutable snapshot** wrapper around a resource value.
+/// It internally holds an `Arc<ResType>`, allowing multiple callers to simultaneously hold read-only
+/// access to the same underlying data without worrying about ownership transfer or lifetime entanglement.
+///
+/// # Why `GlobalResource` is Needed
+///
+/// In the [`GlobalResContainer`] (global resource container), resources are stored in a three-layer
+/// `Arc<Mutex<Arc<Res>>>` structure:
+///
+/// - The outer `Mutex` ensures that only one modifier can exclusively access the resource at a time;
+/// - The innermost `Arc<Res>` provides an **immutable shared snapshot**;
+/// - `GlobalResource` is the safe exposure wrapper around that innermost `Arc<Res>`.
+///
+/// When multiple callers each obtain a `GlobalResource` via [`GlobalResContainer::res`], they share
+/// the same underlying `Arc<Res>`, thus guaranteeing consistency of data between them (all being the
+/// same snapshot).
+///
+/// # Usage
+///
+/// `GlobalResource<ResType>` implements [`Deref`](https://doc.rust-lang.org/stable/core/ops/trait.Deref.html) (with target type `ResType`), so it can be
+/// dereferenced directly like `&ResType` to access the underlying value:
+///
+/// ```
+/// # use mingling_core::GlobalResource;
+/// let res = GlobalResource::new(42i32);
+/// assert_eq!(*res, 42);
+/// ```
+///
+/// It can also be used with [`AsRef`] to obtain a `&ResType` reference:
+///
+/// ```
+/// # use mingling_core::GlobalResource;
+/// let res = GlobalResource::new(String::from("hello"));
+/// assert_eq!(res.as_ref(), "hello");
+/// ```
+///
+/// # Relationship with Modification Operations
+///
+/// - `GlobalResource` provides **read-only** access only; the underlying data is immutable.
+/// - When an external caller holds a `GlobalResource` (causing the `Arc` strong reference count
+/// to be > 1), subsequent [`modify_res`](GlobalResContainer::modify_res) operations on the same
+/// resource will **clone** a copy for modification and **will not affect** the snapshot held here.
+/// - This means that `GlobalResource` can serve as a stable view of the resource, retaining the
+/// data content as of the initial read even after modification operations occur.
+///
+/// # Type Constraints
+///
+/// - `ResType` must satisfy `'static + Send + Sync` to ensure safe sharing across threads.
+/// - The resource itself typically also needs to implement [`ResourceMarker`] (providing default
+/// values, cloning, etc.) so that the container can perform default instantiation and
+/// clone-based modification.
+///
+/// # See Also
+///
+/// - [`GlobalResContainer::res`] — obtain a `GlobalResource` snapshot from the container.
+/// - [`GlobalResContainer::res_or_default`] — obtain a snapshot, or return a default value when missing.
+/// - [`GlobalResContainer::res_or_route`] — obtain a snapshot, or route to a specified path when missing.
+///
+/// [`GlobalResContainer`]: crate::GlobalResContainer
+/// [`GlobalResContainer::res`]: crate::GlobalResContainer::res
+/// [`GlobalResContainer::res_or_default`]: crate::GlobalResContainer::res_or_default
+/// [`GlobalResContainer::res_or_route`]: crate::GlobalResContainer::res_or_route
+/// [`ResourceMarker`]: crate::ResourceMarker
pub struct GlobalResource<ResType: 'static + Send + Sync> {
res_arc: Arc<ResType>,
}
impl<ResType: 'static + Send + Sync> GlobalResource<ResType> {
- /// Create a new `GlobalAsset` from an `AssetType` value.
+ /// Creates a new [`GlobalResource`], wrapping the given value directly.
+ ///
+ /// # Parameters
+ ///
+ /// - `res`: The resource value to wrap. The value must be of a `'static + Send + Sync` type
+ /// to ensure it can be safely shared across threads.
+ ///
+ /// # Return Value
+ ///
+ /// Returns a [`GlobalResource<ResType>`] holding `Arc::new(res)`, which can be dereferenced
+ /// via [`Deref`] or [`AsRef`] to access the underlying value.
+ ///
+ /// # Difference from `From<Arc<ResType>>`
+ ///
+ /// - `new` accepts an **owned value** `ResType`, automatically wrapping it into `Arc<ResType>`;
+ /// - `From<Arc<ResType>>` accepts an **already-wrapped `Arc`**, reusing it directly without an
+ /// additional heap allocation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::GlobalResource;
+ /// let res = GlobalResource::new(42i32);
+ /// assert_eq!(*res, 42);
+ /// ```
+ ///
+ /// # See Also
+ ///
+ /// - [`GlobalResource::from`] (`From<Arc<ResType>>`) — construct from an existing `Arc`.
+ /// - [`Deref`] — dereference to access the underlying value.
+ ///
+ /// [`Deref`]: std::ops::Deref
+ /// [`AsRef`]: std::convert::AsRef
pub fn new(res: ResType) -> Self {
Self {
res_arc: Arc::new(res),
@@ -174,21 +1267,143 @@ 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
+/// Marks a type as a **program global resource** (`Res`) that can be stored in the
+/// [`GlobalResContainer`].
+///
+/// # Purpose
+///
+/// `ResourceMarker` is a marker trait that resource types must implement, providing three fundamental
+/// capabilities:
+///
+/// 1. **Cloning** (`__resource_marker_clone`) — when a resource in the container is held by an external
+/// shared snapshot (such as a [`GlobalResource`]), modification operations need to clone an independent
+/// copy for modification, to avoid affecting the external snapshot.
+/// 2. **Default instantiation** (`__resource_marker_default`) — when a resource entry is missing, or the
+/// lock is poisoned, the container needs a default instance as a fallback value.
+/// 3. **Modification through the global program container** (`__resource_marker_modify`) — locates the
+/// currently active [`Program<C>`] via the type parameter `C` and performs a read-modify-write
+/// operation on the `&mut Self` resource within it.
+///
+/// # Relationship with `Default + Clone`
+///
+/// This trait provides an **automatic blanket implementation** for all types satisfying
+/// `T: Default + Clone + Send + Sync + 'static`, so ordinary user-defined data types do not need to
+/// manually implement `ResourceMarker`. If a type has custom "default value" or "cloning" semantics
+/// (for example, if the resource internally contains `Arc`, `Rc`, singleton references, etc.), users may
+/// also **manually implement** this trait to override the default behavior.
+///
+/// # Comparison of the Three Methods' Uses
+///
+/// | Method | Invocation Scenario | Corresponding Blanket Implementation |
+/// |--------|-------------------|--------------------------------------|
+/// | `__resource_marker_clone` | When the resource is held by a shared snapshot, clone a copy before modification | `Clone::clone` |
+/// | `__resource_marker_default` | Fallback value when the resource is missing or the lock is poisoned | `Default::default` |
+/// | `__resource_marker_modify` | Perform a read-modify-write on `&mut Self` in the global program container | Calls `this::<C>().modify_res(f)` |
+///
+/// # Role in Macro Expansion
+///
+/// In code expanded from `#[chain]`, `#[resource]`, and similar macros, wherever a type needs to be
+/// treated as a resource for injection, modification, extraction, or storage, this trait's constraint
+/// (`Res: ResourceMarker`) is implicitly relied upon. The macro code itself does not care about the
+/// specific type of the resource; it only requires that the type implements the three capabilities
+/// of this trait.
+///
+/// # Constraints
+///
+/// Types implementing this trait must simultaneously satisfy `'static + Send + Sync`, to ensure
+/// that the resource can be safely shared across threads and does not carry a non-static lifetime.
+///
+/// # Note
+///
+/// All methods of this trait are **`#[doc(hidden)]`** internal methods and should not be called directly
+/// in business code. The public APIs exposed are [`GlobalResContainer::with_resource`],
+/// [`GlobalResContainer::modify_res`], [`GlobalResContainer::res`], etc.
+///
+/// [`GlobalResContainer`]: crate::GlobalResContainer
+/// [`GlobalResource`]: crate::GlobalResource
+/// [`Program<C>`]: crate::Program
+/// [`this`]: crate::this
+/// [`GlobalResContainer::with_resource`]: crate::GlobalResContainer::with_resource
+/// [`GlobalResContainer::modify_res`]: crate::GlobalResContainer::modify_res
+/// [`GlobalResContainer::res`]: crate::GlobalResContainer::res
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.
+ /// Clones the current resource value and returns an independent new instance.
+ ///
+ /// # Invocation Scenario
+ ///
+ /// When an external caller holds a shared snapshot of the resource (with an `Arc` strong reference
+ /// count > 1), the container's modification operation (such as [`modify_res`](GlobalResContainer::modify_res))
+ /// cannot directly take ownership of the resource, so this method is called to **clone a copy**
+ /// for modification, ensuring the external snapshot is not affected.
+ ///
+ /// # Implementation Conventions
+ ///
+ /// - Must return an independent instance that is logically equivalent (with the same value) to
+ /// `self`; modifying the return value must not affect the original value.
+ /// - The blanket implementation directly delegates to `Clone::clone(self)`.
+ /// - For types containing reference-counted structures such as `Arc` or `Rc`, the underlying data
+ /// should be deep-cloned rather than merely copying the reference, unless sharing is explicitly
+ /// the intended semantics.
#[must_use]
#[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.
+ /// Constructs a default instance of the type, used as a fallback value when the resource is missing
+ /// or the lock is poisoned.
+ ///
+ /// # Invocation Scenario
+ ///
+ /// This method is called in the following scenarios:
+ /// - When obtaining a resource snapshot via [`res_or_default`](GlobalResContainer::res_or_default),
+ /// but the resource entry does not exist in the container or the lock is poisoned;
+ /// - When performing a read-modify-write via
+ /// [`__modify_res_and_return_route`](GlobalResContainer::__modify_res_and_return_route),
+ /// if the resource entry does not exist or the lock is poisoned, a temporary default instance
+ /// needs to be constructed for use by the closure;
+ /// - When extracting a resource via [`__extract_res_mut`](GlobalResContainer::__extract_res_mut),
+ /// if the resource entry does not exist or the lock is poisoned.
+ ///
+ /// # Implementation Conventions
+ ///
+ /// - Each call should return a **brand new** default instance and accept no parameters.
+ /// - The blanket implementation directly delegates to `Default::default()`.
+ /// - If the type's default value has special semantics (for example, default configuration,
+ /// empty collection, zero value, etc.), this should be reflected here.
#[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.
+ /// Performs a **read-modify-write** operation on a resource of type `Self` in the currently active
+ /// global program container.
+ ///
+ /// # Generic Parameters
+ ///
+ /// - `C`: The program collector type. Must satisfy `ProgramCollect<Enum = C> + 'static`,
+ /// used to locate the currently active [`Program<C>`](crate::Program).
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: A closure receiving `&mut Self`, within which the resource value is modified. The closure's
+ /// return value is not used (returns `()`).
+ ///
+ /// # Behavior
+ ///
+ /// This method is internally equivalent to calling:
+ ///
+ /// ```text
+ /// this::<C>().modify_res(f)
+ /// ```
+ ///
+ /// where `this::<C>()` obtains the thread-bound global [`Program<C>`](crate::Program) instance,
+ /// and then calls its [`modify_res`](crate::Program::modify_res) method to complete the
+ /// read-modify-write. If the resource does not exist, `modify_res` returns `()` (the `Default`
+ /// value of `Return`) without calling `f`.
+ ///
+ /// # Role in Macro Expansion
+ ///
+ /// In code expanded from `#[chain]` and similar macros, when a resource needs to be modified via
+ /// the global program container without explicitly obtaining a container reference, this method is
+ /// called. It automatically locates the correct program instance via the type parameter `C`,
+ /// simplifying code generation logic.
#[doc(hidden)]
fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self))
where
@@ -218,6 +1433,8 @@ where
#[cfg(test)]
mod tests {
use super::*;
+ use crate::MockProgramCollect;
+ use crate::error::ChainProcessError;
#[test]
fn global_resource_new_and_deref() {
@@ -278,8 +1495,229 @@ mod tests {
);
}
- // Note: Tests for Program::with_resource, res(), res_or_route(), res_or_default(),
- // and modify_res() require a concrete ProgramCollect implementation, which is
- // complex and outside the scope of these unit tests.
- // Those are better covered by integration tests.
+ #[test]
+ fn container_new_creates_empty_store() {
+ let container = GlobalResContainer::new();
+ assert!(container.res::<i32>().is_none());
+ }
+
+ #[test]
+ fn container_insert_then_res() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(42i32);
+ let res = container.res::<i32>();
+ assert_eq!(*res.unwrap(), 42);
+ }
+
+ #[test]
+ fn container_missing_res_returns_none() {
+ let container = GlobalResContainer::new();
+ assert!(container.res::<String>().is_none());
+ }
+
+ #[test]
+ fn container_res_or_default_creates_default() {
+ let container = GlobalResContainer::new();
+ assert_eq!(*container.res_or_default::<i32>(), 0);
+ }
+
+ #[test]
+ fn container_res_or_default_returns_existing() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(7i32);
+ assert_eq!(*container.res_or_default::<i32>(), 7);
+ }
+
+ #[test]
+ fn container_modify_res_updates_value() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32);
+ let doubled = container.modify_res(|v: &mut i32| {
+ *v *= 2;
+ *v
+ });
+ assert_eq!(doubled, 2);
+ assert_eq!(*container.res::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn container_modify_res_missing_returns_default() {
+ let container = GlobalResContainer::new();
+ let value: i32 = container.modify_res(|v: &mut i32| *v);
+ assert_eq!(value, 0);
+ }
+
+ #[test]
+ fn container_modify_res_through_shared_reference() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(10i32);
+ let shared = &container;
+ shared.modify_res(|v: &mut i32| *v += 5);
+ assert_eq!(*container.res::<i32>().unwrap(), 15);
+ }
+
+ #[test]
+ fn container_modify_res_clones_when_shared() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(10i32);
+ // Hold a shared handle so `Arc::try_unwrap` fails and the resource is cloned out
+ let handle = container.res::<i32>().unwrap();
+ container.modify_res(|v: &mut i32| *v += 5);
+ assert_eq!(*container.res::<i32>().unwrap(), 15);
+ assert_eq!(*handle, 10);
+ }
+
+ #[test]
+ fn container_extract_res_mut_takes_value_out() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(3i32);
+ let extracted: i32 = container.__extract_res_mut();
+ assert_eq!(extracted, 3);
+ // The slot is reset to a default value after the extraction
+ assert_eq!(*container.res::<i32>().unwrap(), 0);
+ }
+
+ #[test]
+ fn container_extract_res_mut_clones_when_shared() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(3i32);
+ // Hold a shared handle so `Arc::try_unwrap` fails and the resource is cloned out
+ let _handle = container.res::<i32>().unwrap();
+ let extracted: i32 = container.__extract_res_mut();
+ assert_eq!(extracted, 3);
+ // The slot is reset to a default value after the extraction
+ assert_eq!(*container.res::<i32>().unwrap(), 0);
+ }
+
+ #[test]
+ fn container_store_res_inserts_value() {
+ let container = GlobalResContainer::new();
+ container.__store_res(8i32);
+ assert_eq!(*container.res::<i32>().unwrap(), 8);
+ }
+
+ #[test]
+ fn container_extract_store_roundtrip() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(7i32);
+ let value: i32 = container.__extract_res_mut();
+ container.__store_res(value + 1);
+ assert_eq!(*container.res::<i32>().unwrap(), 8);
+ }
+
+ #[test]
+ fn container_res_shared_handles_share_the_arc() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(String::from("hello"));
+ let handle_a = container.res::<String>().unwrap();
+ let handle_b = container.res::<String>().unwrap();
+ assert_eq!(*handle_a, "hello");
+ assert_eq!(*handle_b, "hello");
+ assert!(Arc::ptr_eq(&handle_a.res_arc, &handle_b.res_arc));
+ }
+
+ #[test]
+ fn container_res_or_route_missing_returns_route() {
+ let container = GlobalResContainer::new();
+ let route: ChainProcess<MockProgramCollect> =
+ ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ let result = container.res_or_route::<i32, MockProgramCollect>(route);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn container_res_or_route_present_returns_resource() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(5i32);
+ let route: ChainProcess<MockProgramCollect> =
+ ChainProcess::Err(ChainProcessError::Other("missing".into()));
+ let Ok(resource) = container.res_or_route::<i32, MockProgramCollect>(route) else {
+ panic!("expected the resource to be present");
+ };
+ assert_eq!(*resource, 5);
+ }
+
+ #[test]
+ fn container_modify_res_and_return_route_works() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32);
+ let route: ChainProcess<MockProgramCollect> =
+ container.__modify_res_and_return_route(|v: &mut i32| {
+ *v += 1;
+ ChainProcess::Err(ChainProcessError::Other("done".into()))
+ });
+ assert!(matches!(route, ChainProcess::Err(_)));
+ assert_eq!(*container.res::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn container_nested_modify_res_different_types_do_not_deadlock() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ // Two `&mut` injections nest `modify_res` calls; each resource has its
+ // own mutex, so the inner call must not deadlock against the outer one.
+ container.modify_res(|count: &mut i32| {
+ *count += 10;
+ container.modify_res(|text: &mut String| {
+ text.push('b');
+ });
+ });
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ assert_eq!(*container.res::<String>().unwrap(), "ab");
+ }
+
+ #[test]
+ fn container_nested_modify_res_and_return_route_no_deadlock() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ let route: ChainProcess<MockProgramCollect> =
+ container.__modify_res_and_return_route(|count: &mut i32| {
+ *count += 10;
+ container.__modify_res_and_return_route(|text: &mut String| {
+ text.push('b');
+ ChainProcess::Err(ChainProcessError::Other("done".into()))
+ })
+ });
+ assert!(matches!(route, ChainProcess::Err(_)));
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ assert_eq!(*container.res::<String>().unwrap(), "ab");
+ }
+
+ #[test]
+ fn container_res_inside_modify_of_another_resource() {
+ let mut container = GlobalResContainer::new();
+ container.with_resource(1i32).with_resource("a".to_string());
+ container.modify_res(|count: &mut i32| {
+ // Reading a different resource while holding this one's lock must
+ // not deadlock either.
+ assert_eq!(*container.res::<String>().unwrap(), "a");
+ *count += 10;
+ });
+ assert_eq!(*container.res::<i32>().unwrap(), 11);
+ }
+
+ #[test]
+ fn container_multiple_instances_are_independent() {
+ let mut first = GlobalResContainer::new();
+ let mut second = GlobalResContainer::new();
+ first.with_resource(1i32);
+ second.with_resource(2i32);
+ first.modify_res(|v: &mut i32| *v += 10);
+ assert_eq!(*first.res::<i32>().unwrap(), 11);
+ assert_eq!(*second.res::<i32>().unwrap(), 2);
+ // A resource present in one container is invisible to the other
+ assert!(first.res::<String>().is_none());
+ assert!(second.res::<String>().is_none());
+ }
+
+ #[test]
+ fn program_resource_methods_delegate_to_container() {
+ let mut program = crate::Program::<MockProgramCollect>::new_with_args(Vec::<String>::new());
+ program.with_resource(1i32);
+ assert_eq!(*program.res::<i32>().unwrap(), 1);
+ program.modify_res(|v: &mut i32| *v += 1);
+ assert_eq!(*program.res::<i32>().unwrap(), 2);
+ assert_eq!(*program.res_or_default::<i32>(), 2);
+ assert_eq!(*program.res_or_default::<String>(), "");
+ }
}
diff --git a/mingling_core/src/asset/help.rs b/mingling_core/src/asset/help.rs
index b3742f2..4c20928 100644
--- a/mingling_core/src/asset/help.rs
+++ b/mingling_core/src/asset/help.rs
@@ -1,10 +1,49 @@
use crate::RenderResult;
-/// Handles help rendering for command-line arguments
+/// Mingling's program help request.
+///
+/// It provides help capability to a program by binding an entry type. When [`Program`](https://docs.rs/mingling/latest/mingling/struct.Program.html)'s `user_context.help` is `true`,
+/// the first Entry produced by [`Dispatcher`](https://docs.rs/mingling/latest/mingling/trait.Dispatcher.html) will be sent into [`HelpRequest`] and rendered into a [`RenderResult`] for the user.
+///
+/// # Manual impl
+///
+/// Normally, [`HelpRequest`] is generated by [`#[help]`](https://docs.rs/mingling/latest/mingling/macros/attr.help.html),
+/// but if you need to implement it manually, please follow the example below:
+///
+/// ```
+/// # use mingling_core::HelpRequest;
+/// # use mingling_core::RenderResult;
+/// # use mingling_core::MockProgramCollect as ThisProgram;
+/// struct GreetHelp;
+/// struct EntryGreet;
+///
+/// impl HelpRequest for GreetHelp {
+/// type Entry = EntryGreet;
+///
+/// fn render_help(p: Self::Entry) -> RenderResult {
+/// let mut result = RenderResult::new();
+/// result.eprintln("USAGE: greet <PARAM...>");
+/// result
+/// }
+/// }
+///
+/// // Register help with the program
+/// // mingling::register_help!(EntryGreet, GreetHelp);
+/// ```
pub trait HelpRequest {
- /// The entry type
+ /// The entry type corresponding to this help request.
+ ///
+ /// This associated type indicates which entry the help request will handle. When the program needs to display help,
+ /// the first entry produced by the `Dispatcher` will be passed to the corresponding help request for processing.
type Entry;
- /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html)
+ /// Render the entry as help information.
+ ///
+ /// This function receives an entry of type [`Self::Entry`] and renders it into a [`RenderResult`].
+ /// Implementors should output help content (such as usage instructions, parameter descriptions, etc.)
+ /// into the [`RenderResult`].
+ ///
+ /// # Return
+ /// Returns a [`RenderResult`] containing the help text, ready to be displayed directly to the user by the caller.
fn render_help(p: Self::Entry) -> RenderResult;
}
diff --git a/mingling_core/src/asset/lazy_resource.rs b/mingling_core/src/asset/lazy_resource.rs
index 3cb7563..bc256c3 100644
--- a/mingling_core/src/asset/lazy_resource.rs
+++ b/mingling_core/src/asset/lazy_resource.rs
@@ -1,30 +1,114 @@
use crate::{ProgramCollect, ResourceMarker, this};
+/// Internal state enum for lazily-loaded resources.
+///
+/// This enum represents the two possible states of `LazyRes`:
+/// - [`LazyInner::Uninit`]: The resource has not been initialized, holding an initialization factory function (`FnMut`) and an optional drop callback.
+/// - [`LazyInner::Init`]: The resource has been initialized, holding the actual value `T` and an optional drop callback.
+///
+/// The optional drop callback has type `FnOnce(T)` and is invoked when the resource is dropped,
+/// allowing the user to obtain final ownership of the resource value for cleanup.
enum LazyInner<T> {
- /// Not yet initialized — holds the factory function.
- /// After init, the factory is **dropped**, no wasted memory.
+ /// Uninitialized state.
+ ///
+ /// The first field holds a callable factory function `FnMut()` that lazily creates the resource value `T` when needed.
+ /// The second field holds an optional drop callback `FnOnce(T)`; if set,
+ /// the cleanup logic will run when the resource is dropped (whether or not it has been initialized).
+ ///
+ /// # Thread Safety
+ ///
+ /// Both the factory function and the drop callback must be `Send + Sync` to ensure that `LazyInner` can be safely
+ /// shared or moved between threads.
Uninit(
+ /// The resource initialization factory function. This function is invoked once when the resource is first accessed,
+ /// and is used to produce the actual resource value.
Box<dyn FnMut() -> T + Send + Sync>,
+ /// Optional drop callback. When the resource is dropped (`drop`), if the resource has been initialized,
+ /// the resource value will be passed to this callback so that custom cleanup logic can run.
Option<Box<dyn FnOnce(T) + Send + Sync>>,
),
- /// Initialized and ready to go.
+ /// Initialized state.
+ ///
+ /// The first field holds the actually created resource value `T`.
+ /// The second field holds the optional drop callback `FnOnce(T)`, which is invoked when the resource is dropped,
+ /// receiving the resource value so that custom cleanup logic can run.
Init(T, Option<Box<dyn FnOnce(T) + Send + Sync>>),
}
-/// A lazily initialized resource that only creates its value on first access.
+/// A lazily-loaded program resource.
+///
+/// `LazyRes<T>` is a container that holds a resource value `T`, which is lazily initialized through a factory
+/// function the first time it is accessed. This type is suitable for scenarios where resource creation needs to be
+/// deferred (such as global configuration, database connection pools, render pipelines, etc.), avoiding unnecessary
+/// initialization overhead.
+///
+/// # Features
+///
+/// - **Lazy initialization**: The resource is created only upon the first call to [`LazyRes::get_ref`], [`LazyRes::get_mut`], or
+/// [`LazyRes::get_clone`]; the factory function runs exactly once.
+/// - **Custom drop callback**: A `FnOnce(T)` callback can be set via [`LazyRes::new_with_drop`], [`LazyRes::with_on_drop`],
+/// or [`LazyRes::set_on_drop`], obtaining final ownership of the resource value and executing cleanup logic when the resource is dropped.
+/// - **Thread safety**: `T` must satisfy `Send + Sync`, and the factory function and drop callback must also be `Send + Sync`,
+/// ensuring `LazyRes<T>` can be safely shared or moved between threads.
+/// - **Rich value extraction**: Supports multiple ways to extract the resource value, including [`LazyRes::into_inner`], [`LazyRes::unwrap`],
+/// [`LazyRes::unwrap_or`], and [`LazyRes::unwrap_or_default`].
+///
+/// # Examples
+///
+/// ```
+/// use mingling_core::LazyRes;
///
-/// Unlike `Option<T>` + persistent `Box<dyn FnMut>`, the factory function is
-/// **consumed and dropped** after initialization. No leftover allocation.
+/// // Create a lazily initialized configuration object
+/// let mut config = LazyRes::new(|| {
+/// // Expensive initialization runs here, only once on first access
+/// String::from("default-config")
+/// });
///
-/// Initialization is triggered by `get_ref()`, `get_mut()`, or `get_clone()`.
+/// assert!(!config.is_initialized());
+/// let value = config.get_ref();
+/// assert_eq!(value, "default-config");
+/// assert!(config.is_initialized());
+/// ```
+///
+/// # Performance Considerations
+///
+/// First access requires a `&mut self` mutable borrow because the internal state must transition from uninitialized to
+/// initialized. If the resource is never accessed (i.e., initialization is never triggered), the factory function is never
+/// called and no initialization overhead is incurred.
+///
+/// # Generic Constraints
+///
+/// - `T: Send + Sync + 'static` — The resource value must be safely shareable between threads and must not hold
+/// references with a non-'static lifetime.
+/// - The factory function type is `FnMut() -> T + Send + Sync + 'static`, meaning the factory may be invoked
+/// multiple times (though in practice it runs only once) and must not capture non-'static lifetime references.
pub struct LazyRes<T: Send + Sync + 'static> {
+ /// Internal state, which may be either uninitialized (`LazyInner::Uninit`) or initialized (`LazyInner::Init`).
inner: LazyInner<T>,
}
impl<T: Send + Sync + 'static> LazyRes<T> {
- /// Creates a new lazily initialized resource with a custom initializer.
+ /// Creates a new lazy resource, whose value is initialized via the factory function `f` on first access.
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: The resource initialization factory function. This function runs once on the first call to `get_ref`, `get_mut`, or
+ /// `get_clone`, and is used to produce the actual resource value.
///
- /// The factory `f` is called on first access, then dropped.
+ /// # Returns
+ ///
+ /// Returns a `LazyRes<T>` instance that is not yet initialized.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| String::from("hello"));
+ /// assert!(!res.is_initialized());
+ /// assert_eq!(*res.get_ref(), "hello");
+ /// assert!(res.is_initialized());
+ /// ```
#[must_use]
pub fn new(f: impl FnMut() -> T + Send + Sync + 'static) -> Self {
Self {
@@ -32,9 +116,32 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
}
}
- /// Creates a new lazily initialized resource with a custom initializer and
- /// an optional on-drop callback that receives ownership of the inner value when
- /// the `LazyRes` is dropped.
+ /// Creates a new lazy resource and simultaneously sets a drop callback.
+ ///
+ /// When the resource is dropped (`drop`), if the resource was initialized via the factory function,
+ /// ownership of the resource value will be handed to the `on_drop` callback so that custom cleanup logic can run.
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: The resource initialization factory function, executed on first access.
+ /// - `on_drop`: The drop callback. When the resource is dropped and has been initialized, the resource value is passed to it.
+ ///
+ /// # Returns
+ ///
+ /// Returns a `LazyRes<T>` instance that has a drop callback set and is not yet initialized.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new_with_drop(
+ /// || 42,
+ /// |val| println!("Resource {} is being dropped", val),
+ /// );
+ /// res.get_ref();
+ /// drop(res); // triggers on_drop callback
+ /// ```
#[must_use]
pub fn new_with_drop(
f: impl FnMut() -> T + Send + Sync + 'static,
@@ -45,47 +152,101 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
}
}
- /// Chains an on-drop callback onto the lazy resource, returning ownership.
+ /// Sets a drop callback using a consuming builder-style chained call.
+ ///
+ /// Similar to [`LazyRes::new_with_drop`], but uses a builder style:
+ /// `with_on_drop` allows chaining a drop callback after the resource has been created.
+ /// If a drop callback was previously set, the new callback replaces the old one.
+ ///
+ /// # Parameters
+ ///
+ /// - `self`: The current `LazyRes` instance.
+ /// - `on_drop`: The drop callback, executed when the resource is dropped.
+ ///
+ /// # Returns
///
- /// This method consumes `self` and returns it with the callback attached,
- /// allowing a builder‑style pattern.
+ /// Returns the same `LazyRes` instance with the drop callback set.
///
- /// The callback receives ownership of the inner value when the `LazyRes` is
- /// dropped. If the resource has not yet been initialized, the callback is stored
- /// and will be invoked once initialization happens *and* the `LazyRes` is later
- /// dropped.
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let res = LazyRes::new(|| 100).with_on_drop(|val| {
+ /// println!("Dropping value: {}", val);
+ /// });
+ /// ```
+ #[must_use]
pub fn with_on_drop(mut self, on_drop: impl FnOnce(T) + Send + Sync + 'static) -> Self {
match &mut self.inner {
- LazyInner::Uninit(_, existing_opt) => {
- *existing_opt = Some(Box::new(on_drop));
- }
- LazyInner::Init(_, existing_opt) => {
+ LazyInner::Uninit(_, existing_opt) | LazyInner::Init(_, existing_opt) => {
*existing_opt = Some(Box::new(on_drop));
}
}
self
}
- /// Sets or replaces the on-drop callback for the initialized value.
- /// The callback is called with ownership of the inner value when the `LazyRes` is dropped.
- /// If the resource has not been initialized yet, the callback is stored and applied
- /// upon initialization.
+ /// Sets a drop callback via a mutable reference.
+ ///
+ /// Unlike [`LazyRes::with_on_drop`], `set_on_drop` modifies `&mut self` via mutable borrow,
+ /// without consuming ownership of `self`. If a drop callback was previously set, the new callback replaces the old one.
+ ///
+ /// # Parameters
+ ///
+ /// - `on_drop`: The drop callback, executed when the resource is dropped.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| 42);
+ /// res.set_on_drop(|val| println!("Final value: {}", val));
+ /// ```
pub fn set_on_drop(&mut self, on_drop: impl FnOnce(T) + Send + Sync + 'static) {
match &mut self.inner {
- LazyInner::Uninit(_, existing_opt) => {
- *existing_opt = Some(Box::new(on_drop));
- }
- LazyInner::Init(_, existing_opt) => {
+ LazyInner::Uninit(_, existing_opt) | LazyInner::Init(_, existing_opt) => {
*existing_opt = Some(Box::new(on_drop));
}
}
}
- /// Returns `true` if the resource has been initialized.
- pub fn is_initialized(&self) -> bool {
+ /// Checks whether the resource has been fully initialized.
+ ///
+ /// Returns `true` if the resource has been initialized (i.e., the factory function has already been invoked once).
+ /// Otherwise returns `false`.
+ ///
+ /// # Returns
+ ///
+ /// - `true`: The resource has been initialized.
+ /// - `false`: The resource has not yet been initialized.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| 42);
+ /// assert!(!res.is_initialized());
+ /// res.get_ref();
+ /// assert!(res.is_initialized());
+ /// ```
+ pub const fn is_initialized(&self) -> bool {
matches!(&self.inner, LazyInner::Init(_, _))
}
+ /// Forces initialization of the resource (if not already initialized).
+ ///
+ /// This is an internal method that ensures the resource is in an initialized state. If the resource is already
+ /// initialized, it returns immediately without doing anything; if not yet initialized, it calls the factory function
+ /// to create the resource value and transitions to the [`LazyInner::Init`] state.
+ ///
+ /// If the factory function panics during execution, the internal state is replaced with a "poisoned" placeholder
+ /// value, and any subsequent access to the resource will trigger an `unreachable!()` panic.
+ ///
+ /// # Returns
+ ///
+ /// A `&mut` reference to the initialized internal state.
fn force_init(&mut self) -> &mut LazyInner<T> {
if matches!(&self.inner, LazyInner::Uninit(_, _)) {
// Replace with a temporary poison value so the real factory can be moved out.
@@ -109,27 +270,79 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
&mut self.inner
}
- /// Returns an immutable reference to the inner value,
- /// calling the initializer on first access.
+ /// Obtains an immutable reference to the resource.
+ ///
+ /// If the resource has not been initialized, this method first invokes the factory function to complete lazy
+ /// initialization, then returns an immutable reference to the resource value.
+ ///
+ /// # Returns
+ ///
+ /// An immutable reference `&T` to the resource value.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| String::from("config"));
+ /// assert_eq!(res.get_ref(), "config");
+ /// ```
pub fn get_ref(&mut self) -> &T {
self.force_init();
match &self.inner {
LazyInner::Init(t, _) => t,
- _ => unreachable!(),
+ LazyInner::Uninit(..) => unreachable!(),
}
}
- /// Returns a mutable reference to the inner value,
- /// calling the initializer on first access.
+ /// Obtains a mutable reference to the resource.
+ ///
+ /// If the resource has not been initialized, this method first invokes the factory function to complete lazy
+ /// initialization, then returns a mutable reference to the resource value, allowing modification of the resource.
+ ///
+ /// # Returns
+ ///
+ /// A mutable reference `&mut T` to the resource value.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| 10);
+ /// *res.get_mut() = 20;
+ /// assert_eq!(*res.get_ref(), 20);
+ /// ```
pub fn get_mut(&mut self) -> &mut T {
self.force_init();
match &mut self.inner {
LazyInner::Init(t, _) => t,
- _ => unreachable!(),
+ LazyInner::Uninit(..) => unreachable!(),
}
}
- /// Returns a clone of the inner value, calling the initializer if necessary.
+ /// Obtains a cloned copy of the resource value.
+ ///
+ /// If the resource has not been initialized, this method first completes lazy initialization, then clones the
+ /// resource value and returns it. This method requires `T` to implement the [`Clone`] trait.
+ ///
+ /// # Returns
+ ///
+ /// A cloned copy `T` of the resource value.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `T: Clone` — The resource type must support cloning.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| vec![1, 2, 3]);
+ /// let cloned = res.get_clone();
+ /// assert_eq!(cloned, vec![1, 2, 3]);
+ /// ```
pub fn get_clone(&mut self) -> T
where
T: Clone,
@@ -137,10 +350,31 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
self.get_ref().clone()
}
- /// Consumes the lazy resource and returns the inner value, if initialized.
+ /// Extracts the internal resource value, returning `Option<T>`.
+ ///
+ /// Consumes the current `LazyRes`; if the resource has been initialized, returns `Some(value)`;
+ /// if the resource has not been initialized, returns `None`.
///
- /// Unlike `reset()`, this **drops** the lazy wrapper entirely.
- /// If you need to re-initialize, just construct a new `LazyRes::new(f)`.
+ /// Note: Unlike [`LazyRes::unwrap`], this method does not force initialization,
+ /// nor does it invoke the drop callback.
+ ///
+ /// # Returns
+ ///
+ /// - `Some(T)`: The resource has been initialized; returns the internal resource value.
+ /// - `None`: The resource has not been initialized.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| 7);
+ /// res.get_ref();
+ /// assert_eq!(res.into_inner(), Some(7));
+ ///
+ /// let res2 = LazyRes::<i32>::new(|| 7);
+ /// assert_eq!(res2.into_inner(), None);
+ /// ```
pub fn into_inner(mut self) -> Option<T> {
// Take a temporary replacement to avoid moving out of a Drop type.
let inner = std::mem::replace(
@@ -153,10 +387,28 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
}
}
- /// Consumes the lazy resource and returns the inner value.
+ /// Unwraps the internal resource value, panicking if the resource has not been initialized.
+ ///
+ /// Consumes the current `LazyRes` and returns the internal resource value `T`. Unlike [`LazyRes::into_inner`],
+ /// this method requires the resource to already be initialized; otherwise it triggers a panic.
+ ///
+ /// # Panics
+ ///
+ /// Calling this method when the resource has not been initialized will trigger a panic.
+ ///
+ /// # Returns
///
- /// If the resource has not been initialized, the initializer is called first.
- /// This is different from `into_inner()` which returns `None` if uninitialized.
+ /// The internal resource value `T`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::new(|| 13);
+ /// res.get_ref();
+ /// assert_eq!(res.unwrap(), 13);
+ /// ```
pub fn unwrap(mut self) -> T {
match std::mem::replace(
&mut self.inner,
@@ -169,7 +421,31 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
}
}
- /// Consumes the lazy resource, calling the initializer first if not yet initialized.
+ /// Unwraps the internal resource value, returning a default value if the resource has not been initialized.
+ ///
+ /// Consumes the current `LazyRes`; if the resource has been initialized, returns the internal resource value `T`;
+ /// otherwise returns the provided `default` value as a substitute. This method does not force initialization.
+ ///
+ /// # Parameters
+ ///
+ /// - `default`: The substitute value returned when the resource has not been initialized.
+ ///
+ /// # Returns
+ ///
+ /// The internal resource value if the resource has been initialized; otherwise `default`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let res: LazyRes<i32> = LazyRes::new(|| 5);
+ /// assert_eq!(res.unwrap_or(100), 100);
+ ///
+ /// let mut res2 = LazyRes::new(|| 5);
+ /// res2.get_ref();
+ /// assert_eq!(res2.unwrap_or(100), 5);
+ /// ```
pub fn unwrap_or(mut self, default: T) -> T {
if matches!(&self.inner, LazyInner::Uninit(_, _)) {
default
@@ -179,14 +455,35 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
LazyInner::Uninit(Box::new(|| unreachable!()), None),
) {
LazyInner::Init(t, _) => t,
- _ => unreachable!(),
+ LazyInner::Uninit(..) => unreachable!(),
}
}
}
- /// Consumes the lazy resource, calling the initializer first if not yet initialized.
+ /// Unwraps the internal resource value, returning `T::default()` if the resource has not been initialized.
+ ///
+ /// Consumes the current `LazyRes`; if the resource has been initialized, returns the internal resource value `T`;
+ /// otherwise returns `T::default()` as a substitute. This method does not force initialization.
+ ///
+ /// This is a convenience wrapper around [`LazyRes::unwrap_or`], using `T::default()` as the
+ /// default value.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `T: Default` — The resource type must implement the [`Default`] trait.
+ ///
+ /// # Returns
///
- /// If the resource has not been initialized, `T::default()` is used as the fallback.
+ /// The internal resource value if the resource has been initialized; otherwise `T::default()`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let res: LazyRes<i32> = LazyRes::new(|| 42);
+ /// assert_eq!(res.unwrap_or_default(), 0);
+ /// ```
pub fn unwrap_or_default(self) -> T
where
T: Default,
@@ -216,18 +513,13 @@ impl<T: Send + Sync + 'static> Drop for LazyRes<T> {
}
}
-impl<T: Send + Sync + 'static> Default for LazyRes<T>
-where
- T: Default,
-{
- /// Creates an uninitialized `LazyRes<T>` whose initializer returns `T::default()`.
+impl<T: Send + Sync + Default + 'static> Default for LazyRes<T> {
fn default() -> Self {
Self::new(|| T::default())
}
}
impl<T: Send + Sync + 'static> From<T> for LazyRes<T> {
- /// Creates a `LazyRes<T>` from an already-initialized value.
fn from(value: T) -> Self {
Self {
inner: LazyInner::Init(value, None),
@@ -236,7 +528,29 @@ impl<T: Send + Sync + 'static> From<T> for LazyRes<T> {
}
impl<T: Send + Sync + 'static> LazyRes<T> {
- /// Creates a lazily initialized resource using `T::default()` as the initializer.
+ /// Creates a lazily initialized default resource.
+ ///
+ /// This is an alias method for [`LazyRes::default`], using `T::default()` as the factory function's return value.
+ /// Like [`LazyRes::new`], the resource value is created only upon first access.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `T: Default` — The resource type must implement the [`Default`] trait.
+ ///
+ /// # Returns
+ ///
+ /// Returns a not-yet-initialized `LazyRes<T>` instance whose factory function will return `T::default()`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::<i32>::lazy_default();
+ /// assert!(!res.is_initialized());
+ /// assert_eq!(*res.get_ref(), 0);
+ /// ```
+ #[must_use]
pub fn lazy_default() -> Self
where
T: Default,
@@ -244,18 +558,101 @@ impl<T: Send + Sync + 'static> LazyRes<T> {
Self::default()
}
- /// Creates a lazily initialized resource with a custom initializer.
+ /// Creates a lazily initialized resource using the specified factory function.
+ ///
+ /// This is an alias method for [`LazyRes::new`], providing a name more aligned with "lazy initialization" semantics.
+ /// The factory function `f` runs once when the resource is first accessed, producing the actual resource value.
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: The resource initialization factory function. This function runs once on the first call to `get_ref`, `get_mut`, or
+ /// `get_clone`, and is used to produce the actual resource value.
+ ///
+ /// # Returns
///
- /// Same as `LazyRes::new`.
+ /// Returns a not-yet-initialized `LazyRes<T>` instance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyRes;
+ ///
+ /// let mut res = LazyRes::lazy_init(|| String::from("lazy"));
+ /// assert!(!res.is_initialized());
+ /// assert_eq!(*res.get_ref(), "lazy");
+ /// ```
pub fn lazy_init(f: impl FnMut() -> T + Send + Sync + 'static) -> Self {
Self::new(f)
}
}
-/// Provides convenience methods for types, allowing them to easily
-/// create a corresponding `LazyRes<T>`.
+/// A trait that provides convenient lazy-initialization capabilities.
+///
+/// The `LazyInit` trait provides a set of convenience methods for all types satisfying `Send + Sync + 'static`
+/// to create [`LazyRes`]-wrapped resources. Any type satisfying those constraints can directly call
+/// [`LazyInit::lazy_default`] or [`LazyInit::lazy_init`] by type name,
+/// without needing to explicitly write the more verbose `LazyRes<T>::new(...)`.
+///
+/// # Implementation
+///
+/// This trait is automatically implemented for all `T: Send + Sync + 'static` types
+/// (via `impl<T: Send + Sync + 'static> LazyInit for T {}`),
+/// so manual implementation is generally unnecessary.
+///
+/// # Method 1: `lazy_default`
+///
+/// Creates a `LazyRes<T>` using `T::default()` as the factory function's return value.
+/// Requires `T` to implement the [`Default`] trait.
+///
+/// ```
+/// use mingling_core::{LazyRes, LazyInit};
+///
+/// let mut res = i32::lazy_default();
+/// assert!(!res.is_initialized());
+/// assert_eq!(*res.get_ref(), 0);
+/// ```
+///
+/// # Method 2: `lazy_init`
+///
+/// Creates a `LazyRes<T>` using a custom factory function. The factory function executes when
+/// the resource is first accessed to produce the actual value.
+///
+/// ```
+/// use mingling_core::{LazyRes, LazyInit};
+///
+/// let mut res = String::lazy_init(|| String::from("hello"));
+/// assert!(!res.is_initialized());
+/// assert_eq!(*res.get_ref(), "hello");
+/// ```
+///
+/// # Generic Constraints
+///
+/// - `Self: Send + Sync + 'static` — The type must be safely shareable between threads and must not hold
+/// non-'static lifetime references.
pub trait LazyInit: Send + Sync + 'static {
- /// Creates a lazily initialized resource for this type using `Default` as the initializer.
+ /// Creates a lazily initialized default resource.
+ ///
+ /// Uses `Self::default()` as the factory function, returning a not-yet-initialized
+ /// [`LazyRes<Self>`](LazyRes). The resource value is created via `Self::default()` upon first access.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `Self: Default` — The type must implement the [`Default`] trait.
+ ///
+ /// # Returns
+ ///
+ /// Returns a not-yet-initialized `LazyRes<Self>` instance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyInit;
+ ///
+ /// let mut res = i32::lazy_default();
+ /// assert!(!res.is_initialized());
+ /// assert_eq!(*res.get_ref(), 0);
+ /// ```
+ #[must_use]
fn lazy_default() -> LazyRes<Self>
where
Self: Default,
@@ -263,7 +660,32 @@ pub trait LazyInit: Send + Sync + 'static {
LazyRes::default()
}
- /// Creates a lazily initialized resource for this type with a custom initializer.
+ /// Creates a lazily initialized resource using a custom factory function.
+ ///
+ /// The factory function `f` runs once when the resource is first accessed to produce the actual value.
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: The resource initialization factory function. This function runs once on the first call to `get_ref`, `get_mut`, or
+ /// `get_clone`, and is used to produce the actual resource value.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `Self: Sized` — The type must have a known size.
+ ///
+ /// # Returns
+ ///
+ /// Returns a not-yet-initialized `LazyRes<Self>` instance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::LazyInit;
+ ///
+ /// let mut res = String::lazy_init(|| String::from("hello"));
+ /// assert!(!res.is_initialized());
+ /// assert_eq!(*res.get_ref(), "hello");
+ /// ```
fn lazy_init(f: impl FnMut() -> Self + Send + Sync + 'static) -> LazyRes<Self>
where
Self: Sized,
@@ -274,12 +696,20 @@ pub trait LazyInit: Send + Sync + 'static {
impl<T: Send + Sync + 'static> LazyInit for T {}
-impl<T: Send + Sync + 'static> ResourceMarker for LazyRes<T>
-where
- T: Default + Clone,
-{
- /// Clones the lazy resource. The cloned resource retains any initialized value,
- /// but the initializer is reset to `T::default()`.
+impl<T: Send + Sync + 'static + Default + Clone> ResourceMarker for LazyRes<T> {
+ /// Clones a deep copy of the current lazy resource as either initialized or default state.
+ ///
+ /// If the resource has been initialized, clones the internal resource value and returns a new initialized
+ /// `LazyRes<T>`; if the resource has not been initialized, returns a default `LazyRes<T>` using `T::default()`
+ /// as the factory function.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `T: Clone` — The resource type must implement the [`Clone`] trait.
+ ///
+ /// # Returns
+ ///
+ /// A cloned `LazyRes<T>` instance. The cloned instance does not carry the original instance's drop callback.
fn __resource_marker_clone(&self) -> Self {
match &self.inner {
LazyInner::Init(t, _) => Self {
@@ -289,15 +719,34 @@ where
}
}
- /// Returns a default lazy resource (uninitialized, using `T::default()` as the initializer).
- fn __resource_marker_default() -> Self
- where
- T: Default,
- {
+ /// Creates a default lazy resource instance.
+ ///
+ /// Returns a not-yet-initialized `LazyRes<T>` using `T::default()` as the factory function.
+ /// The resource value is created only upon first access.
+ ///
+ /// # Type Constraints
+ ///
+ /// - `T: Default` — The resource type must implement the [`Default`] trait.
+ ///
+ /// # Returns
+ ///
+ /// A not-yet-initialized default `LazyRes<T>` instance.
+ fn __resource_marker_default() -> Self {
Self::default()
}
- /// Modifies the current lazy resource via the `this` context provided by `C`.
+ /// Modifies the current lazy resource via `ProgramCollect`.
+ ///
+ /// Uses the provided closure `f` to modify the current `LazyRes<T>`. The modification is delegated
+ /// to the `ProgramCollect` resource collector via `this::<C>().modify_res(f)`.
+ ///
+ /// # Parameters
+ ///
+ /// - `f`: The closure used to modify `LazyRes<T>`, receiving a `&mut Self` parameter.
+ ///
+ /// # Generic Constraints
+ ///
+ /// - `C: ProgramCollect<Enum = C> + 'static` — The program collector type; the `Enum` associated type must equal itself.
fn __resource_marker_modify<C>(f: impl FnOnce(&mut Self))
where
C: ProgramCollect<Enum = C> + 'static,
@@ -311,8 +760,6 @@ mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
-
- /// Helper for tracking drops via an `Arc<AtomicBool>`.
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
diff --git a/mingling_core/src/asset/metadata.rs b/mingling_core/src/asset/metadata.rs
new file mode 100644
index 0000000..4ac758b
--- /dev/null
+++ b/mingling_core/src/asset/metadata.rs
@@ -0,0 +1,37 @@
+/// Provides metadata for an Entry.
+///
+/// Any type can be attached to an Entry as metadata, allowing the program to
+/// carry compile-time-typed, arbitrary description data alongside each
+/// registered entry. The [`Metadata`] trait bridges an Entry type (`Self`) to
+/// an arbitrary metadata type `DataType`.
+///
+/// # Manual impl
+///
+/// It is recommended to use the `#[metadata(Entry)]` attribute macro from
+/// [mingling_macros](https://crates.io/crates/mingling_macros) to implement this
+/// trait and register the entry via `register_metadata!`.
+///
+/// If you need to implement it manually, you can refer to the following
+/// example:
+///
+/// ```
+/// # use mingling_core::MockProgramCollect as ThisProgram;
+/// # use mingling_core::Metadata;
+/// struct EntryGreet;
+/// struct MyInformation {
+/// name: String
+/// }
+///
+/// impl Metadata<MyInformation> for EntryGreet {
+/// fn init_metadata() -> MyInformation {
+/// MyInformation { name: "Greeting".into() }
+/// }
+/// }
+///
+/// // Register the MyInformation metadata for EntryGreet using `register_metadata!`.
+/// // mingling::macros::register_metadata!(EntryGreet, MyInformation);
+/// ```
+pub trait Metadata<DataType> {
+ /// Initializes and returns the metadata value of type `DataType` for this entry.
+ fn init_metadata() -> DataType;
+}
diff --git a/mingling_core/src/asset/node.rs b/mingling_core/src/asset/node.rs
deleted file mode 100644
index caf34ec..0000000
--- a/mingling_core/src/asset/node.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-use just_fmt::kebab_case;
-
-/// Represents a command node, used to match user-input command paths.
-///
-/// The node consists of multiple parts, each separated by a dot (`.`), and automatically converted to kebab-case.
-/// For example, the input string `"node.subnode"` will be converted to a node representation of `["node", "subnode"]`.
-#[derive(Debug, Default)]
-pub struct Node {
- node: Vec<String>,
-}
-
-impl Node {
- /// Append a new part to the node path.
- #[must_use]
- 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("."))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_node_from_single_str() {
- let node = Node::from("hello");
- assert_eq!(node.node, vec!["hello"]);
- assert_eq!(node.to_string(), "hello");
- }
-
- #[test]
- fn test_node_from_dotted_str() {
- let node = Node::from("a.b.c");
- assert_eq!(node.node, vec!["a", "b", "c"]);
- assert_eq!(node.to_string(), "a.b.c");
- }
-
- #[test]
- fn test_node_kebab_case_conversion() {
- let node = Node::from("HelloWorld.FooBar");
- assert_eq!(node.node, vec!["hello-world", "foo-bar"]);
- }
-
- #[test]
- fn test_node_from_string() {
- let s = String::from("x.y");
- let node = Node::from(s);
- assert_eq!(node.node, vec!["x", "y"]);
- }
-
- #[test]
- fn test_node_join() {
- let node = Node::from("base").join("sub");
- assert_eq!(node.node, vec!["base", "sub"]);
- }
-
- #[test]
- fn test_node_join_multiple() {
- let node = Node::from("a").join("b").join("c");
- assert_eq!(node.to_string(), "a.b.c");
- }
-
- #[test]
- fn test_node_default_empty() {
- let node = Node::default();
- assert!(node.node.is_empty());
- assert_eq!(node.to_string(), "");
- }
-
- #[test]
- fn test_node_partial_eq() {
- let a = Node::from("a.b");
- let b = Node::from("a.b");
- let c = Node::from("a.c");
- assert_eq!(a, b);
- assert_ne!(a, c);
- }
-
- #[test]
- fn test_node_ord() {
- let a = Node::from("a");
- let b = Node::from("b");
- assert!(a < b);
- }
-
- #[test]
- fn test_node_join_appends_part() {
- let node = Node::from("existing");
- let joined = node.join("new-part");
- assert_eq!(joined.to_string(), "existing.new-part");
- }
-}
diff --git a/mingling_core/src/asset/renderer.rs b/mingling_core/src/asset/renderer.rs
index 732f9b7..f6dcbd0 100644
--- a/mingling_core/src/asset/renderer.rs
+++ b/mingling_core/src/asset/renderer.rs
@@ -1,10 +1,35 @@
use crate::RenderResult;
-/// Takes over a type (`Self::Previous`) and converts it to a [`RenderResult`](./struct.RenderResult.html)
+/// Rendering logic for Mingling programs
+///
+/// Add a rendering type for registered types in the Mingling program. When they are routed to `to_render()`, this renderer will be invoked to render them into the result output.
+///
+/// # Manual impl
+///
+/// Generally speaking, it is recommended to use the [`#[renderer]`](https://docs.rs/mingling/latest/mingling/macros/attr.renderer.html) macro instead.
+/// If you need to implement this manually, please refer to the following example:
+///
+/// ```
+/// # use mingling_core::Renderer;
+/// # use mingling_core::RenderResult;
+/// # struct MyRenderer;
+/// # struct StateMyType;
+///
+/// impl Renderer for MyRenderer {
+/// type Previous = StateMyType;
+///
+/// fn render(prev: Self::Previous) -> RenderResult {
+/// // The specific rendering logic
+/// # return mingling_core::RenderResult::default();
+/// }
+/// }
+/// ```
pub trait Renderer {
- /// The previous type in the chain
+ /// The previous type handled by the renderer, used to convert it into a render result
type Previous;
- /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html)
+ /// The rendering logic, which converts the `Previous` type into the corresponding [`RenderResult`] output
+ ///
+ /// When a program is routed to the type registered for this renderer, this method will be called to convert and render the previous type into the final result.
fn render(p: Self::Previous) -> RenderResult;
}
diff --git a/mingling_core/src/asset/routable.rs b/mingling_core/src/asset/routable.rs
index 24b7bb1..7381b48 100644
--- a/mingling_core/src/asset/routable.rs
+++ b/mingling_core/src/asset/routable.rs
@@ -1,22 +1,112 @@
-use crate::ChainProcess;
+use crate::{AnyOutput, ChainProcess, Grouped, ProgramCollect};
-/// Provides routing capabilities for converting an item into a `ChainProcess`
-/// directed to either the chain or render processing pipeline.
+/// Represents a type that can be routed within a group.
///
-/// This trait enables items to be dispatched to different processing routes
-/// (chain or render) by wrapping them into an `AnyOutput` and routing them
-/// through the appropriate pipeline.
+/// Used to indicate that a group member can be routed into another [`ChainProcess`]
+/// within the execution logic of a [`Chain`](https://docs.rs/mingling/latest/mingling/trait.Chain.html).
+///
+/// # Blanket impl
+///
+/// When a type implements [`Grouped`], it automatically gets a corresponding [`Routable`]
+/// implementation, meaning all types deriving [`Grouped`] can flow through the program loop.
+///
+/// # Reference
+///
+/// You can use the [`routeify`](https://docs.rs/mingling/latest/mingling/macros/attr.routeify.html)
+/// macro and the [`route!`](https://docs.rs/mingling/latest/mingling/macros/macro.route.html) macro
+/// to build flexible program execution logic.
+///
+/// # Example
+///
+/// ```
+/// # use mingling_core::Routable;
+/// # use mingling_core::ChainProcess;
+/// # use mingling_core::MockProgramCollect as ThisProgram;
+/// # use mingling_core::Grouped;
+/// # unsafe impl Grouped<ThisProgram> for Foo {
+/// # fn member_id() -> ThisProgram {
+/// # ThisProgram::Foo
+/// # }
+/// # }
+/// struct Foo;
+///
+/// // With `Grouped` implemented, the type automatically implements `Routable`
+/// // and can be converted into a `ChainProcess` via `to_chain` / `to_render`:
+/// fn takes_chain<T: Routable<ThisProgram>>(value: T) -> ChainProcess<ThisProgram> {
+/// value.to_chain()
+/// }
+///
+/// # fn main() {
+/// # takes_chain(Foo);
+/// # }
+/// ```
pub trait Routable<Group>
where
Self: Sized + 'static,
{
- /// Converts the routable item into a `ChainProcess` directed to the chain route.
+ /// Converts the current type into a [`ChainProcess`] that can be used for execution in a program chain (`Chain`).
+ ///
+ /// # Return value
+ ///
+ /// Returns a [`ChainProcess`] wrapping the current value,
+ /// which will be scheduled for execution in the program chain.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{Grouped, ChainProcess};
+ /// # use mingling_core::MockProgramCollect as ThisProgram;
+ /// # unsafe impl Grouped<ThisProgram> for StateMyType {
+ /// # fn member_id() -> ThisProgram {
+ /// # ThisProgram::Foo
+ /// # }
+ /// # }
+ /// use mingling_core::Routable;
///
- /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline.
+ /// struct StateMyType;
+ ///
+ /// let my_type = StateMyType;
+ /// let process: ChainProcess<ThisProgram> = my_type.to_chain();
+ /// ```
fn to_chain(self) -> ChainProcess<Group>;
- /// Converts the routable item into a `ChainProcess` directed to the render route.
+ /// Converts the current type into a [`ChainProcess`] that can be used for the rendering pipeline.
+ ///
+ /// # Return value
+ ///
+ /// Returns a [`ChainProcess`] wrapping the current value,
+ /// which will be scheduled for execution in the rendering pipeline.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use mingling_core::{Grouped, ChainProcess};
+ /// # use mingling_core::MockProgramCollect as ThisProgram;
+ /// # unsafe impl Grouped<ThisProgram> for StateMyType {
+ /// # fn member_id() -> ThisProgram {
+ /// # ThisProgram::Foo
+ /// # }
+ /// # }
+ /// use mingling_core::Routable;
+ ///
+ /// struct StateMyType;
///
- /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline.
+ /// let my_type = StateMyType;
+ /// let process: ChainProcess<ThisProgram> = my_type.to_render();
+ /// ```
fn to_render(self) -> ChainProcess<Group>;
}
+
+impl<T, C> Routable<C> for T
+where
+ C: ProgramCollect<Enum = C>,
+ T: Grouped<C> + Send,
+{
+ fn to_chain(self) -> ChainProcess<C> {
+ AnyOutput::new(self).route_chain()
+ }
+
+ fn to_render(self) -> ChainProcess<C> {
+ AnyOutput::new(self).route_renderer()
+ }
+}