From 618e22e38b3a5349724f6fd610f9da8d2c1e44cd Mon Sep 17 00:00:00 2001 From: Marc Hartmayer Date: Wed, 27 May 2026 15:56:07 +0200 Subject: [PATCH] utils: Add utils_macros: Implement ValueEnumDisplay and ValueEnumFromStr macros Add new derive macros ValueEnumDisplay and ValueEnumFromStr which can be used to derive 'Display' and 'FromStr' for enums implementing 'clap::ValueEnum'. It helps in reducing boilerplate code and keep things in sync. Assisted-by: IBM Bob:1.0.4 Signed-off-by: Marc Hartmayer Reviewed-by: Steffen Eiden Signed-off-by: Steffen Eiden --- rust/Cargo.lock | 11 +++ rust/Cargo.toml | 1 + rust/utils/Cargo.toml | 2 + rust/utils/src/lib.rs | 2 + rust/utils_macros/Cargo.toml | 17 ++++ rust/utils_macros/src/lib.rs | 158 +++++++++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+) create mode 100644 rust/utils_macros/Cargo.toml create mode 100644 rust/utils_macros/src/lib.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b4ebf866..7d7d2f5f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1225,6 +1225,17 @@ dependencies = [ "s390_pv", "serde", "serde_json", + "utils_macros", +] + +[[package]] +name = "utils_macros" +version = "0.12.0" +dependencies = [ + "clap", + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 660b530b..d382feeb 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,6 +11,7 @@ members = [ "pvsecret", "pvverify", "utils", + "utils_macros", ] resolver = "2" diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml index 4da43d0f..b828c124 100644 --- a/rust/utils/Cargo.toml +++ b/rust/utils/Cargo.toml @@ -12,5 +12,7 @@ log = { version = "0.4.29", features = ["std", "release_max_level_debug"] } pv = { path = "../pv", package = "s390_pv" } serde = { version = "1.0.228"} +utils_macros = { path = "../utils_macros" } + [dev-dependencies] serde_json = "1.0.149" diff --git a/rust/utils/src/lib.rs b/rust/utils/src/lib.rs index 7252ed7f..5e98c76e 100644 --- a/rust/utils/src/lib.rs +++ b/rust/utils/src/lib.rs @@ -13,6 +13,8 @@ mod log; mod tmpfile; pub use ::log::LevelFilter; +// Re-export procedural macros from utils_macros +pub use utils_macros::{ValueEnumDisplay, ValueEnumFromStr}; pub use crate::cli::{ combined_path_opt, combined_path_req, get_reader_from_cli_file_arg, diff --git a/rust/utils_macros/Cargo.toml b/rust/utils_macros/Cargo.toml new file mode 100644 index 00000000..db535759 --- /dev/null +++ b/rust/utils_macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "utils_macros" +version = "0.12.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full", "extra-traits"] } +quote = "1.0" +proc-macro2 = "1.0" + +[dev-dependencies] +clap = { version = "4.6", features = ["derive"] } diff --git a/rust/utils_macros/src/lib.rs b/rust/utils_macros/src/lib.rs new file mode 100644 index 00000000..dca6e530 --- /dev/null +++ b/rust/utils_macros/src/lib.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. + +//! Procedural macros for the utils crate. +//! +//! This crate provides derive macros to reduce boilerplate in enum definitions. + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +/// Derive `std::fmt::Display` for enums implementing `clap::ValueEnum`. +/// +/// This macro generates a `Display` implementation that delegates to +/// `ValueEnum::to_possible_value()`, ensuring that the formatted output +/// matches the CLI representation used by clap (e.g. for help text, +/// completions, and parsing). +/// +/// # Behavior +/// +/// - Uses the canonical CLI name of each variant (as defined by `#[value(name = "...")]` or the +/// default casing). +/// - Fails at runtime if a variant is marked with `#[value(skip)]` and therefore has no CLI +/// representation. +/// +/// # Example +/// +/// ```rust +/// use clap::ValueEnum; +/// use utils_macros::ValueEnumDisplay; +/// +/// #[derive(ValueEnum, ValueEnumDisplay, Clone)] +/// enum Mode { +/// #[value(name = "very-fast")] +/// Fast, +/// +/// #[value(name = "slow")] +/// Slow, +/// } +/// +/// assert_eq!(Mode::Fast.to_string(), "very-fast"); +/// ``` +/// +/// # Rationale +/// +/// clap requires `Display` for features like `default_value_t`. However, +/// `ValueEnum` already defines the canonical string representation via +/// `to_possible_value()`. This derive avoids duplicating those strings +/// and guarantees consistency between parsing, help output, and display. +/// +/// # Panics +/// +/// Panics if called on a variant with `#[value(skip)]`, as such variants +/// have no associated CLI representation. +/// +/// # See also +/// +/// - [`clap::ValueEnum`] +/// - [`clap::builder::PossibleValue`] +#[proc_macro_derive(ValueEnumDisplay)] +pub fn derive_value_enum_display(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let expanded = quote! { + impl std::fmt::Display for #name { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + let value = self + .to_possible_value() + .expect("skipped ValueEnum variant cannot be displayed"); + + write!(f, "{}", value.get_name()) + } + } + }; + + expanded.into() +} + +/// Derives a `std::str::FromStr` implementation for enums implementing +/// [`clap::ValueEnum`]. +/// +/// This macro generates a `FromStr` implementation that delegates to +/// [`ValueEnum::from_str`], ensuring that parsing behavior is identical +/// to clap's CLI parsing. +/// +/// # Behavior +/// +/// - Parses input strings using the canonical CLI representation defined by `ValueEnum` (including +/// `#[value(name = "...")]` and aliases). +/// - Supports the same parsing semantics as clap (e.g. case sensitivity, if enabled). +/// - Returns a human-readable error if parsing fails. +/// +/// # Example +/// +/// ```rust +/// use clap::ValueEnum; +/// use utils_macros::ValueEnumFromStr; +/// +/// #[derive(ValueEnum, ValueEnumFromStr, Clone, Debug, PartialEq)] +/// enum Mode { +/// #[value(name = "fast")] +/// Fast, +/// +/// #[value(name = "slow")] +/// Slow, +/// } +/// +/// assert_eq!("fast".parse::().unwrap(), Mode::Fast); +/// assert!("invalid".parse::().is_err()); +/// ``` +/// +/// # Rationale +/// +/// clap's [`ValueEnum`] trait already defines the canonical mapping +/// between strings and enum variants. This derive avoids duplicating +/// that logic in manual `FromStr` implementations and guarantees that +/// CLI parsing and programmatic parsing remain consistent. +/// +/// # Errors +/// +/// Returns an error if the input does not match any of the allowed values +/// defined by `ValueEnum`. +/// +/// # See also +/// +/// - [`clap::ValueEnum`] +/// - [`std::str::FromStr`] +#[proc_macro_derive(ValueEnumFromStr)] +pub fn derive_value_enum_from_str(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let expanded = quote! { + impl std::str::FromStr for #name { + type Err = String; + + fn from_str(s: &str) -> Result { + ::from_str(s, false).map_err(|_| { + let possible = ::value_variants() + .iter() + .filter_map(|v| v.to_possible_value()) + .map(|v| v.get_name().to_string()) + .collect::>() + .join(", "); + + format!("invalid value '{}', expected one of: {}", s, possible) + }) + } + } + }; + + expanded.into() +}