diff options
| -rw-r--r-- | CHANGELOG.md | 9 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/asset.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/asset/help.rs | 10 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 1 | ||||
| -rw-r--r-- | mingling_core/src/program.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/program/exec.rs | 35 | ||||
| -rw-r--r-- | mingling_core/src/renderer/render_result.rs | 15 | ||||
| -rw-r--r-- | mingling_macros/src/help.rs | 202 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 27 |
10 files changed, 309 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ba927..cd2ffc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,15 @@ struct YourCommandEntry { 2. **\[core\]** Added function `new_with_args` to `Program` 3. **\[core\]** Added function `dispatch_args_dynamic` to `Program` +4. **\[core\]** Impl `std::io::Write` trait for `RenderResult` +5. **\[core\]** Added Help system, which allows binding an event for `--help` to an `Entry` via the `help!` macro + +```rust +#[help] +fn your_command_help(_prev: YourEntry) { + r_println!("Your help docs"); +} +``` #### **BREAKING CHANGES**: diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index d712ff0..5240682 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -84,6 +84,8 @@ pub mod macros { pub use mingling_macros::dispatcher_clap; /// Used to collect data and create a command-line context pub use mingling_macros::gen_program; + /// Used to generate a struct implementing the `HelpRequest` trait via a method + pub use mingling_macros::help; /// Used to create a `Node` struct via a literal pub use mingling_macros::node; /// Used to create a wrapper type for use with `Chain` and `Renderer` @@ -103,6 +105,8 @@ pub mod macros { pub use mingling_macros::r_println; /// Used to register a chain pub use mingling_macros::register_chain; + /// Used to register a help + pub use mingling_macros::register_help; /// Used to register a renderer pub use mingling_macros::register_renderer; /// Used to register a type into the context diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs index a4254ef..234fec1 100644 --- a/mingling_core/src/asset.rs +++ b/mingling_core/src/asset.rs @@ -16,3 +16,6 @@ pub mod node; #[doc(hidden)] pub mod renderer; + +#[doc(hidden)] +pub mod help; diff --git a/mingling_core/src/asset/help.rs b/mingling_core/src/asset/help.rs new file mode 100644 index 0000000..ff2b3d4 --- /dev/null +++ b/mingling_core/src/asset/help.rs @@ -0,0 +1,10 @@ +use crate::RenderResult; + +/// Handles help rendering for command-line arguments +pub trait HelpRequest { + /// The entry type + type Entry; + + /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html) + fn render_help(p: Self::Entry, r: &mut RenderResult); +} diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 87be806..aee0df6 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -25,6 +25,7 @@ pub use crate::asset::chain::*; pub use crate::asset::comp::*; pub use crate::asset::dispatcher::*; pub use crate::asset::enum_tag::*; +pub use crate::asset::help::*; pub use crate::asset::node::*; pub use crate::asset::renderer::*; diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs index c9cffbe..949dd45 100644 --- a/mingling_core/src/program.rs +++ b/mingling_core/src/program.rs @@ -289,6 +289,9 @@ pub trait ProgramCollect { /// Render the input [AnyOutput](./struct.AnyOutput.html) fn render(any: AnyOutput<Self::Enum>, r: &mut RenderResult); + /// Render help for Entry + fn render_help(any: AnyOutput<Self::Enum>, r: &mut RenderResult); + /// Find a matching chain to continue execution based on the input [AnyOutput](./struct.AnyOutput.html), returning a new [AnyOutput](./struct.AnyOutput.html) #[cfg(feature = "async")] fn do_chain( diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs index c1eada5..469f6d2 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -16,6 +16,11 @@ where let mut current = dispatch_args_dynamic(program, program.args.clone())?; let mut stop_next = false; + // If the program has Help enabled, skip actual logic and jump to Help + if program.user_context.help { + return Ok(render_help::<C>(program, current)); + } + loop { let final_exec = stop_next; @@ -56,6 +61,11 @@ where let mut current = dispatch_args_dynamic(program, program.args.clone())?; let mut stop_next = false; + // If the program has Help enabled, skip actual logic and jump to Help + if program.user_context.help { + return Ok(render_help::<C>(program, current)); + } + loop { let final_exec = stop_next; @@ -179,3 +189,28 @@ fn render<C: ProgramCollect<Enum = C>>(program: &Program<C>, any: AnyOutput<C>) } } } + +#[inline(always)] +#[allow(unused_variables)] +fn render_help<C: ProgramCollect<Enum = C>>( + program: &Program<C>, + entry: AnyOutput<C>, +) -> RenderResult { + #[cfg(not(feature = "general_renderer"))] + { + let mut render_result = RenderResult::default(); + C::render_help(entry, &mut render_result); + render_result + } + #[cfg(feature = "general_renderer")] + { + match program.general_renderer_name { + super::GeneralRendererSetting::Disable => { + let mut render_result = RenderResult::default(); + C::render_help(entry, &mut render_result); + render_result + } + _ => RenderResult::default(), + } + } +} diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index d9da7b7..3bde1ab 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -1,5 +1,6 @@ use std::{ fmt::{Display, Formatter}, + io::Write, ops::Deref, }; @@ -9,6 +10,20 @@ pub struct RenderResult { render_text: String, } +impl Write for RenderResult { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + let s = std::str::from_utf8(buf).map_err(|_| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, "not valid UTF-8") + })?; + self.render_text.push_str(s); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + impl Display for RenderResult { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}", self.render_text.trim()) diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs new file mode 100644 index 0000000..c0d8b99 --- /dev/null +++ b/mingling_macros/src/help.rs @@ -0,0 +1,202 @@ +//! Help Attribute Macro +//! +//! This module provides the `#[help]` attribute macro for automatically +//! generating structs that implement the `HelpRequest` trait from functions. +//! +//! # Syntax +//! +//! ```rust,ignore +//! #[help] +//! fn help_my_entry(prev: MyEntry) { +//! // use r_println! here +//! r_println!("Help: ..."); +//! } +//! ``` +//! +//! This expands to: +//! - A struct `HelpMyEntry` implementing `HelpRequest` with `Entry = MyEntry` +//! - The original function with injected `RenderResult` dummy context + +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{ + FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input, +}; + +/// Extracts the previous type and parameter name from function arguments +fn extract_previous_info(sig: &Signature) -> syn::Result<(Pat, TypePath)> { + // The function should have exactly one parameter + if sig.inputs.len() != 1 { + return Err(syn::Error::new( + sig.inputs.span(), + "Help function must have exactly one parameter (the entry type)", + )); + } + + // First and only parameter is the entry type + let arg = &sig.inputs[0]; + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + // Extract the pattern (parameter name) + let param_pat = (**pat).clone(); + + // Extract the type + match &**ty { + Type::Path(type_path) => Ok((param_pat, type_path.clone())), + _ => Err(syn::Error::new( + ty.span(), + "Parameter type must be a type path", + )), + } + } + FnArg::Receiver(_) => Err(syn::Error::new( + arg.span(), + "Help function cannot have self parameter", + )), + } +} + +/// Validates the return type is () or empty +fn validate_return_type(sig: &Signature) -> syn::Result<()> { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()), + _ => Err(syn::Error::new( + ty.span(), + "Help function must return () or have no return type", + )), + }, + ReturnType::Default => Ok(()), + } +} + +pub fn help_attr(item: TokenStream) -> TokenStream { + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Help function cannot be async") + .to_compile_error() + .into(); + } + + // Extract the entry type and parameter name from function arguments + let (prev_param, entry_type) = match extract_previous_info(&input_fn.sig) { + Ok(info) => info, + Err(e) => return e.to_compile_error().into(), + }; + + // Validate return type + if let Err(e) = validate_return_type(&input_fn.sig) { + return e.to_compile_error().into(); + } + + // Get the function body + let fn_body = &input_fn.block; + + // Get function attributes (excluding the help attribute) + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("help")); + + // Get function visibility + let vis = &input_fn.vis; + + // Get function name + let fn_name = &input_fn.sig.ident; + + // Generate struct name from function name using pascal_case + let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string()); + let struct_name = Ident::new(&pascal_case_name, fn_name.span()); + + // Register the help request mapping + let help_entry = build_help_entry(&struct_name, &entry_type); + let entry_str = help_entry.to_string(); + crate::HELP_REQUESTS.lock().unwrap().insert(entry_str); + + // Generate the struct and HelpRequest implementation + let expanded = quote! { + #(#fn_attrs)* + #[doc(hidden)] + #vis struct #struct_name; + + impl ::mingling::HelpRequest for #struct_name { + type Entry = #entry_type; + + fn render_help(#prev_param: Self::Entry, r: &mut ::mingling::RenderResult) { + // Create a local wrapper function that includes r parameter + // This allows r_println! to access r + #[allow(non_snake_case)] + fn help_wrapper(#prev_param: #entry_type, r: &mut ::mingling::RenderResult) { + #fn_body + } + + // Call the wrapper function + help_wrapper(#prev_param, r); + } + } + + ::mingling::macros::register_help!(#entry_type, #struct_name); + + // Keep the original function for internal use (without r parameter) + #(#fn_attrs)* + #vis fn #fn_name(#prev_param: #entry_type) { + let mut dummy_r = ::mingling::RenderResult::default(); + let r = &mut dummy_r; + #fn_body + } + }; + + expanded.into() +} + +/// Builds a help request entry for the global help requests list +fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::TokenStream { + let enum_variant = &entry_type.path.segments.last().unwrap().ident; + quote! { + Self::#enum_variant => { + // SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`, + // so downcasting to `#entry_type` is safe. + let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() }; + <#struct_name as ::mingling::HelpRequest>::render_help(value, r); + } + } +} + +pub fn register_help(input: TokenStream) -> TokenStream { + // Parse the input as a comma-separated list of arguments + let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); + + // Check that we have exactly two elements + if input_parsed.len() != 2 { + return syn::Error::new( + input_parsed.span(), + "Expected exactly two comma-separated arguments: `EntryType, StructName`", + ) + .to_compile_error() + .into(); + } + + // Extract the two elements + let entry_type_expr = &input_parsed[0]; + let struct_name_expr = &input_parsed[1]; + + // Convert expressions to TypePath and Ident + let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) { + Ok(ty) => ty, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) { + Ok(ident) => ident, + Err(e) => return e.to_compile_error().into(), + }; + + // Register the help request mapping + let help_entry = build_help_entry(&struct_name, &entry_type); + let entry_str = help_entry.to_string(); + crate::HELP_REQUESTS.lock().unwrap().insert(entry_str); + + quote! {}.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index da3bf47..dbbc333 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -19,6 +19,7 @@ mod dispatcher; mod dispatcher_clap; mod enum_tag; mod groupped; +mod help; mod node; mod pack; mod program_setup; @@ -44,6 +45,8 @@ pub(crate) static CHAINS_EXIST: Lazy<Mutex<BTreeSet<String>>> = Lazy::new(|| Mutex::new(BTreeSet::new())); pub(crate) static RENDERERS_EXIST: Lazy<Mutex<BTreeSet<String>>> = Lazy::new(|| Mutex::new(BTreeSet::new())); +pub(crate) static HELP_REQUESTS: Lazy<Mutex<BTreeSet<String>>> = + Lazy::new(|| Mutex::new(BTreeSet::new())); #[proc_macro] pub fn node(input: TokenStream) -> TokenStream { @@ -97,6 +100,16 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { dispatcher_clap::dispatcher_clap_attr(attr, item) } +#[proc_macro] +pub fn register_help(input: TokenStream) -> TokenStream { + help::register_help(input) +} + +#[proc_macro_attribute] +pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { + help::help_attr(item) +} + #[proc_macro_derive(Groupped, attributes(group))] pub fn derive_groupped(input: TokenStream) -> TokenStream { groupped::derive_groupped(input) @@ -308,6 +321,14 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream { #[cfg(not(feature = "comp"))] let comp = quote! {}; + let help_tokens: Vec<proc_macro2::TokenStream> = HELP_REQUESTS + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + let expanded = quote! { #[derive(Debug, Default, PartialEq, Eq, Clone)] #[repr(u32)] @@ -340,6 +361,12 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream { ::mingling::__dispatch_program_chains!( #(#chain_tokens)* ); + fn render_help(any: ::mingling::AnyOutput<Self::Enum>, r: &mut ::mingling::RenderResult) { + match any.member_id { + #(#help_tokens)* + _ => (), + } + } fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { match any.member_id { #(#renderer_exist_tokens)* |
