diff options
| -rw-r--r-- | CHANGELOG.md | 30 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | docs/example-pages/examples.json | 14 | ||||
| -rw-r--r-- | examples/example-lazy-resources/Cargo.lock | 76 | ||||
| -rw-r--r-- | examples/example-lazy-resources/Cargo.toml | 8 | ||||
| -rw-r--r-- | examples/example-lazy-resources/page.toml | 10 | ||||
| -rw-r--r-- | examples/example-lazy-resources/src/main.rs | 92 | ||||
| -rw-r--r-- | examples/test-examples.toml | 10 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 108 | ||||
| -rw-r--r-- | mingling_core/src/asset.rs | 9 | ||||
| -rw-r--r-- | mingling_core/src/asset/lazy_resource.rs | 141 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 1 | ||||
| -rw-r--r-- | mling/src/cli.rs | 15 | ||||
| -rw-r--r-- | mling/src/pkg_mgr/installer.rs | 0 | ||||
| -rw-r--r-- | mling/src/pkg_mgr/mod.rs | 3 | ||||
| -rw-r--r-- | mling/src/proj_mgr/metadata.rs | 8 |
16 files changed, 517 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 95db724..bc74d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,36 @@ program.with_setup(BasicProgramSetup); 3. **\[core\]** Added `verbose`, `quiet`, `debug`, `color`, and `progress` fields to `ProgramStdoutSetting`, and `dry_run`, `force`, `interactive`, and `assume_yes` fields to `ProgramUserContext`. These fields are annotated as conventions only, meaning the framework does not enforce any particular behavior — it is up to the application to read and act on them. +4. **\[core\]** Added `LazyRes<T>` for lazy resource initialization. Resources wrapped in `LazyRes<T>` are only initialized when first accessed via `get_ref()` or `get_mut()`, rather than immediately when added to the program. This is useful for resources that are expensive to initialize and may not always be needed. + +```rust +use std::collections::BTreeMap; +use mingling::{LazyInit, LazyRes, prelude::*}; + +#[derive(Default, Clone)] +pub struct ResLargeData { + pub data: BTreeMap<String, String>, +} + +fn init_res_large_data() -> ResLargeData { + // Expensive initialization here + ResLargeData { data: BTreeMap::new() } +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResLargeData::lazy_init(init_res_large_data)); + // ... +} + +// Injected as &mut LazyRes<T> instead of &T +#[renderer] +fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) { + let res = res.get_ref(); // Initialization happens here + // use res... +} +``` + #### **BREAKING CHANGES** (API CHANGES): 1. **\[core\]** Changed the signature of `ProgramSetup::setup` from `fn setup(&mut self, program: &mut Program<C>) -> S` to `fn setup(self, program: &mut Program<C>)`, consuming `self` instead of taking a mutable reference. Correspondingly, `Program::with_setup` now accepts `S` by value (`&mut self, setup: S`) instead of by mutable reference (`&mut self, setup: &mut S`). @@ -16,6 +16,7 @@ exclude = [ "examples/example-help", "examples/example-hook", "examples/example-implicit-dispatcher", + "examples/example-lazy-resources", "examples/example-panic-unwind", "examples/example-repl-basic", "examples/example-resources", diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json index 4db968c..9022066 100644 --- a/docs/example-pages/examples.json +++ b/docs/example-pages/examples.json @@ -205,6 +205,20 @@ ] }, { + "id": "example-lazy-resources", + "name": "Lazy Resources", + "icon": "💤️", + "category": "advanced", + "desc": "Example Lazy Resources\n", + "tags": [ + "LazyRes<Res>" + ], + "files": [ + "src/main.rs", + "Cargo.toml" + ] + }, + { "id": "example-panic-unwind", "name": "Panic Unwind", "icon": "💥", diff --git a/examples/example-lazy-resources/Cargo.lock b/examples/example-lazy-resources/Cargo.lock new file mode 100644 index 0000000..eb7d9b3 --- /dev/null +++ b/examples/example-lazy-resources/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-lazy-resources" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "mingling" +version = "0.2.0" +dependencies = [ + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling_core" +version = "0.2.0" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.2.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/examples/example-lazy-resources/Cargo.toml b/examples/example-lazy-resources/Cargo.toml new file mode 100644 index 0000000..1f47f8e --- /dev/null +++ b/examples/example-lazy-resources/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "example-lazy-resources" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" +features = [] diff --git a/examples/example-lazy-resources/page.toml b/examples/example-lazy-resources/page.toml new file mode 100644 index 0000000..d2c740f --- /dev/null +++ b/examples/example-lazy-resources/page.toml @@ -0,0 +1,10 @@ +[example] +id = "example-lazy-resources" +name = "Lazy Resources" +icon = "💤️" +category = "advanced" +desc = """ +Example Lazy Resources +""" +tags = ["LazyRes<Res>"] +files = ["src/main.rs", "Cargo.toml"] diff --git a/examples/example-lazy-resources/src/main.rs b/examples/example-lazy-resources/src/main.rs new file mode 100644 index 0000000..3533ae2 --- /dev/null +++ b/examples/example-lazy-resources/src/main.rs @@ -0,0 +1,92 @@ +//! Example Lazy Resources +//! +//! > This example demonstrates how to use `LazyRes` for lazy resource initialization. +//! +//! Run: +//! ```bash +//! cargo run --manifest-path examples/example-lazy-resources/Cargo.toml --quiet none +//! +//! cargo run --manifest-path examples/example-lazy-resources/Cargo.toml --quiet show +//! ``` +//! +//! Output: +//! ```plaintext +//! None +//! +//! Initialized +//! foo: bar +//! rust: lang +//! baz: qux +//! hello: world +//! key: value +//! ``` + +use std::collections::BTreeMap; + +use mingling::{LazyInit, LazyRes, prelude::*}; + +type Key = String; +type Value = String; + +// Define a resource that requires time-consuming initialization +#[derive(Default, Clone)] +pub struct ResLargeData { + pub data: BTreeMap<Key, Value>, +} + +fn init_res_large_data() -> ResLargeData { + // Perform time-consuming initialization here + let mut data = BTreeMap::new(); + data.insert("foo".to_string(), "bar".to_string()); + data.insert("baz".to_string(), "qux".to_string()); + data.insert("hello".to_string(), "world".to_string()); + data.insert("rust".to_string(), "lang".to_string()); + data.insert("key".to_string(), "value".to_string()); + + // Print to indicate initialization is complete + println!("Initialized"); + ResLargeData { data } +} + +dispatcher!("show", CMDShow => EntryShow); +dispatcher!("none", CMDNone => EntryNone); + +pack!(ResultShow = BTreeMap<Key, Value>); + +fn main() { + let mut program = ThisProgram::new(); + + // --------- IMPORTANT --------- + // _ Use lazy_init to create LazyRes<ResLargeData> + // / + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + program.with_resource(ResLargeData::lazy_init(init_res_large_data)); + // --------- IMPORTANT --------- + + program.with_dispatchers((CMDShow, CMDNone)); + program.exec_and_exit(); +} + +// Inject LazyRes instead of a normal resource +// __________________________ Must use &mut because `get_ref` and `get_mut` +// / both require mutable borrow +// | _____________________ Use LazyRes<ResLargeData> +// | / instead of ResLargeData +#[renderer] // vvvv vvvvvvvvvvvvvvvvvvvvv +fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) { + // _______ Initialization happens here + // / + // vvvvvvv + let res = res.get_ref(); + for (key, value) in &res.data { + r_println!("{}: {}", key, value); + } +} + +// When not using LazyRes<ResLargeData>, it will not be initialized +#[renderer] +fn render_entry_none(_args: EntryNone) { + r_println!("None"); +} + +gen_program!(); diff --git a/examples/test-examples.toml b/examples/test-examples.toml index 6f45048..8d2cf0d 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -1,3 +1,13 @@ +[[test.example-lazy-resources]] +command = "none" +expect.exit-code = 0 +expect.result = "None" + +[[test.example-lazy-resources]] +command = "show" +expect.exit-code = 0 +expect.result = "Initialized\nbaz: qux\nfoo: bar\nhello: world\nkey: value\nrust: lang" + [[test.example-basic]] command = "greet" expect.exit-code = 0 diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 3755553..f14aa30 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1412,6 +1412,114 @@ pub mod example_hook {} /// gen_program!(); /// ``` pub mod example_implicit_dispatcher {} +/// Example Lazy Resources +/// +/// > This example demonstrates how to use `LazyRes` for lazy resource initialization. +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-lazy-resources/Cargo.toml --quiet none +/// +/// cargo run --manifest-path examples/example-lazy-resources/Cargo.toml --quiet show +/// ``` +/// +/// Output: +/// ```plaintext +/// None +/// +/// Initialized +/// foo: bar +/// rust: lang +/// baz: qux +/// hello: world +/// key: value +/// ``` +/// +/// Source code (./Cargo.toml) +/// ```toml +/// [package] +/// name = "example-lazy-resources" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = [] +/// ``` +/// +/// Source code (./src/main.rs) +/// ```ignore +/// use std::collections::BTreeMap; +/// +/// use mingling::{LazyInit, LazyRes, prelude::*}; +/// +/// type Key = String; +/// type Value = String; +/// +/// // Define a resource that requires time-consuming initialization +/// #[derive(Default, Clone)] +/// pub struct ResLargeData { +/// pub data: BTreeMap<Key, Value>, +/// } +/// +/// fn init_res_large_data() -> ResLargeData { +/// // Perform time-consuming initialization here +/// let mut data = BTreeMap::new(); +/// data.insert("foo".to_string(), "bar".to_string()); +/// data.insert("baz".to_string(), "qux".to_string()); +/// data.insert("hello".to_string(), "world".to_string()); +/// data.insert("rust".to_string(), "lang".to_string()); +/// data.insert("key".to_string(), "value".to_string()); +/// +/// // Print to indicate initialization is complete +/// println!("Initialized"); +/// ResLargeData { data } +/// } +/// +/// dispatcher!("show", CMDShow => EntryShow); +/// dispatcher!("none", CMDNone => EntryNone); +/// +/// pack!(ResultShow = BTreeMap<Key, Value>); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // --------- IMPORTANT --------- +/// // _ Use lazy_init to create LazyRes<ResLargeData> +/// // / +/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +/// program.with_resource(ResLargeData::lazy_init(init_res_large_data)); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatchers((CMDShow, CMDNone)); +/// program.exec_and_exit(); +/// } +/// +/// // Inject LazyRes instead of a normal resource +/// // __________________________ Must use &mut because `get_ref` and `get_mut` +/// // / both require mutable borrow +/// // | _____________________ Use LazyRes<ResLargeData> +/// // | / instead of ResLargeData +/// #[renderer] // vvvv vvvvvvvvvvvvvvvvvvvvv +/// fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) { +/// // _______ Initialization happens here +/// // / +/// // vvvvvvv +/// let res = res.get_ref(); +/// for (key, value) in &res.data { +/// r_println!("{}: {}", key, value); +/// } +/// } +/// +/// // When not using LazyRes<ResLargeData>, it will not be initialized +/// #[renderer] +/// fn render_entry_none(_args: EntryNone) { +/// r_println!("None"); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_lazy_resources {} /// Example Panic Unwind /// /// > This example introduces how to catch Panic in the Mingling program loop diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index e7945ce..b1fd617 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -11,10 +11,13 @@ pub mod enum_tag; pub mod global_resource; #[doc(hidden)] -pub mod node; +pub mod help; #[doc(hidden)] -pub mod renderer; +pub mod lazy_resource; #[doc(hidden)] -pub mod help; +pub mod node; + +#[doc(hidden)] +pub mod renderer; diff --git a/mingling_core/src/asset/lazy_resource.rs b/mingling_core/src/asset/lazy_resource.rs new file mode 100644 index 0000000..41f8a34 --- /dev/null +++ b/mingling_core/src/asset/lazy_resource.rs @@ -0,0 +1,141 @@ +use crate::{ProgramCollect, ResourceMarker, this}; + +/// A lazily initialized resource that only creates its value on first access via a given initializer function. +/// +/// `LazyRes<T>` wraps an `Option<T>` and an initializer function, supporting thread-safe lazy loading. +/// Initialization is triggered by calls to `get_ref()`, `get_mut()`, or `get_clone()`. +pub struct LazyRes<T: Default + Send + Sync + Clone> { + /// The initializer function, called on first access to produce the inner value. + init_fn: Box<dyn FnMut() -> T + Send + Sync>, + /// Stores the initialized value; `None` means not yet initialized. + inner: Option<T>, +} + +impl<T: Default + Send + Sync + Clone> Default for LazyRes<T> { + /// Creates an uninitialized `LazyRes<T>` whose initializer returns `T::default()`. + fn default() -> Self { + Self { + inner: None, + init_fn: Box::new(|| T::default()), + } + } +} + +impl<T: Default + Send + Sync + Clone + 'static> LazyRes<T> { + /// Creates a new lazily initialized resource with a custom initializer function. + /// + /// # Parameters + /// - `f`: The initializer function called on first access, returning a value of type `T`. + /// + /// # Returns + /// Returns an uninitialized `LazyRes<T>`. + #[must_use] + pub fn new(f: impl FnMut() -> T + Send + Sync + 'static) -> Self { + Self { + inner: None, + init_fn: Box::new(f), + } + } + + /// Returns an immutable reference to the inner value, calling the initializer if necessary. + /// + /// # Returns + /// An immutable reference to the inner value `T`. + pub fn get_ref(&mut self) -> &T { + if self.inner.is_none() { + self.inner = Some((self.init_fn)()); + } + self.inner.as_ref().unwrap() + } + + /// Returns a mutable reference to the inner value, calling the initializer if necessary. + /// + /// # Returns + /// A mutable reference to the inner value `T`. + pub fn get_mut(&mut self) -> &mut T { + if self.inner.is_none() { + self.inner = Some((self.init_fn)()); + } + self.inner.as_mut().unwrap() + } + + /// Resets the lazy resource by taking and returning the inner value; on next access, re-initialization occurs. + /// + /// # Returns + /// Returns the previously initialized value if any, otherwise `None`. + pub fn reset(&mut self) -> Option<T> { + self.inner.take() + } + + /// Returns a clone of the inner value, calling the initializer if necessary. + /// + /// # Returns + /// A clone of type `T`. + pub fn get_clone(&mut self) -> T { + self.get_ref().clone() + } +} + +impl<T: Default + Send + Sync + Clone + 'static> From<T> for LazyRes<T> { + /// Creates a `LazyRes<T>` from an existing value (already initialized), with the initializer set to `T::default()`. + fn from(value: T) -> Self { + Self { + inner: Some(value), + init_fn: Box::new(|| T::default()), + } + } +} + +impl<T: Default + Send + Sync + Clone + 'static> LazyRes<T> { + /// Creates a lazily initialized resource using `T::default()` as the initializer. + pub fn lazy_default() -> Self { + Self::default() + } + + /// Creates a lazily initialized resource with a custom initializer function. + /// + /// Same as `LazyRes::new`. + pub fn lazy_init(f: impl FnMut() -> T + Send + Sync + 'static) -> Self { + Self::new(f) + } +} + +/// Provides convenience methods for types implementing `Default + Send + Sync + Clone + 'static`, +/// allowing them to easily create a corresponding `LazyRes<T>`. +pub trait LazyInit: Default + Send + Sync + Clone + 'static { + /// Creates a lazily initialized resource for this type using `Default` as the initializer. + fn lazy_default() -> LazyRes<Self> { + LazyRes::default() + } + + /// Creates a lazily initialized resource for this type with a custom initializer function. + fn lazy_init(f: impl FnMut() -> Self + Send + Sync + 'static) -> LazyRes<Self> { + LazyRes::new(f) + } +} + +impl<T: Default + Send + Sync + Clone + 'static> LazyInit for T {} + +impl<T: Default + Send + Sync + Clone + 'static> ResourceMarker for LazyRes<T> { + /// Clones the lazy resource. The cloned resource retains any initialized value, but the initializer is reset to `T::default()`. + fn res_clone(&self) -> Self { + Self { + inner: self.inner.clone(), + // The original initializer is not preserved on clone + init_fn: Box::new(|| T::default()), + } + } + + /// Returns a default lazy resource (uninitialized, using `T::default()` as the initializer). + fn res_default() -> Self { + Self::default() + } + + /// Modifies the current lazy resource via the `this` context provided by `C`. + fn modify<C>(f: impl FnOnce(&mut Self)) + where + C: ProgramCollect<Enum = C> + 'static, + { + this::<C>().modify_res(f); + } +} diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 6e1e90c..ddb5446 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -30,6 +30,7 @@ pub use crate::asset::dispatcher::*; pub use crate::asset::enum_tag::*; pub use crate::asset::global_resource::*; pub use crate::asset::help::*; +pub use crate::asset::lazy_resource::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; diff --git a/mling/src/cli.rs b/mling/src/cli.rs index 21f7220..6a4c7d8 100644 --- a/mling/src/cli.rs +++ b/mling/src/cli.rs @@ -116,6 +116,13 @@ fn standard_output_setup(program: &mut Program<ThisProgram>) { } fn resolve_manifest_path(provided: Option<String>) -> PathBuf { + let p = ThisProgram::this(); + let disable_error_output = + // --no-error + !p.stdout_setting.error_output || + // or --quiet/--silence + p.stdout_setting.quiet; + if let Some(path) = provided { let p = PathBuf::from_str(&path).unwrap(); if p.is_dir() { @@ -123,7 +130,9 @@ fn resolve_manifest_path(provided: Option<String>) -> PathBuf { if candidate.exists() { return candidate; } - eprintln_cargo!("`{}` is not a crate root", p.display()); + if !disable_error_output { + eprintln_cargo!("`{}` is not a crate root", p.display()); + } exit(1); } return p; @@ -137,7 +146,9 @@ fn resolve_manifest_path(provided: Option<String>) -> PathBuf { } if !dir.pop() { // Reached filesystem root without finding Cargo.toml - eprintln_cargo!("`{}` is not a crate root", current_dir().unwrap().display()); + if !disable_error_output { + eprintln_cargo!("`{}` is not a crate root", current_dir().unwrap().display()); + } exit(1); } } diff --git a/mling/src/pkg_mgr/installer.rs b/mling/src/pkg_mgr/installer.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mling/src/pkg_mgr/installer.rs diff --git a/mling/src/pkg_mgr/mod.rs b/mling/src/pkg_mgr/mod.rs index d03e1d8..030529e 100644 --- a/mling/src/pkg_mgr/mod.rs +++ b/mling/src/pkg_mgr/mod.rs @@ -4,6 +4,9 @@ use mingling::{ macros::{dispatcher, program_setup}, }; +mod installer; +pub use installer::*; + dispatcher!("install"); dispatcher!("ls.namespace", CMDListNamespace => EntryListNamespace); dispatcher!("rm.namespace", CMDRemoveNamespace => EntryRemoveNamespace); diff --git a/mling/src/proj_mgr/metadata.rs b/mling/src/proj_mgr/metadata.rs index 84da69f..1ba24e1 100644 --- a/mling/src/proj_mgr/metadata.rs +++ b/mling/src/proj_mgr/metadata.rs @@ -16,10 +16,10 @@ pub fn read_metadata(cargo_toml: &PathBuf) -> Result<CargoLockFile, std::io::Err if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("cargo metadata failed: {}", stderr), - )); + return Err(std::io::Error::other(format!( + "cargo metadata failed: {}", + stderr + ))); } let lock_file: CargoLockFile = serde_json::from_slice(&output.stdout) |
