A macro for generating prompts to read an enum/struct

Hi, Rust beginner here. I'd like to make a macro (and have something like this kind of working) whose behavior I will describe in a second, and have a couple questions on it.

The questions will be:

  1. General code-review feedback, if you have any comments on the generated code, overall idea, or API.
  2. I'd like to know if there's a way to derive the functions I want after the definition of the enum/struct.

The macro. The idea is you mark your enum/struct with a derive macro:

#[derive(Promptable)]
enum TheEnum {
    First,
    Second { value1: i32, value2: i32 },
}

and the macro will generate a function that you can use as such:

let the_enum = TheEnum::prompt();
println!("\nYou entered: {the_enum:?}");

and then will lead to an interaction like the following:

Enter variant name [First|Second]: Second
Enter a number: 5
Enter a number: 10

You entered: Second { value1: 5, value2: 10 }

(It'll eventually give better prompts than this, but I want to keep it simpler.)

Right now, the code I'd like to generate in the macro (I haven't fully implemented this yet) is the following:

#[allow(non_snake_case)]
fn _prompt_enum__TheEnum__First() -> TheEnum {
    TheEnum::First {}
}

#[allow(non_snake_case)]
fn _prompt_enum__TheEnum__Second() -> TheEnum {
    let value1 = i32::prompt();
    let value2 = i32::prompt();
    TheEnum::Second { value1, value2 }
}

impl TheEnum {
    fn prompt() -> TheEnum {
        let mut input_line = String::new();
        // This prompt could be customized via an attribute on the enum
        print!("Enter variant name [First|Second]: ");
        io::stdout().flush().expect("Failure flushing");
        io::stdin().read_line(&mut input_line).expect("Failed to read line");
        let input_line = input_line.trim();

        match input_line {
            "First" => _prompt_enum__TheEnum__First(),
            "Second" => _prompt_enum__TheEnum__Second(),
            _ => panic!(),
        }
    }
}

Then I have a library providing the "root" prompt functions. I had to do some searching for how to make it work for primitive types, but I eventually came up with:

trait PromptableI32 {
    fn prompt() -> i32;
}

impl PromptableI32 for i32 {
    fn prompt() -> i32 {
        let mut input_line = String::new();
        print!("Enter a number: ");
        io::stdout().flush().expect("Failure flushing");
        io::stdin().read_line(&mut input_line).expect("Failed to read line");
        let x: i32 = input_line.trim().parse().expect("Input not an integer");
        x    
    }
}

Onto the questions.

First, any general, unprompted feedback about anything I've said so far. If it's about specifics of reading in input I'd be a bit interested, but in reality I'm using the inquire crate for this and just wanted to keep it simple, so that's not directly applicable.

But the bigger question (and why put this in "help" instead of "code review") is that ideally I would really like to not have to put the Promptable derive directly on the definition of TheEnum.

In the full program, TheEnum is defined in a core crate and is a structure used for passing messages around the system. I would rather it not know about input methods itself, make the core crate depend on inquire, and in fact I have possible future ideas for other front ends that would use totally different means for creating variants (e.g. a Discord bot).

Is there something I can do in a separate crate to generate the prompt functions with a generate_promptable!(TheEnum); statement or something like that?

I am willing to modify the definition of TheEnum to make this supportable, but I don't know what to do. I almost wonder if I could convert the proc_macro::TokenStream into a proc_macro2::TokenStream and then make that accessible later on. Other than that, my best idea is to define a new struct/enum in the second crate that is identical except for adding a derive macro, and then also generate conversion functions back and forth. Then I could add a step to the build to just copy the defining file over.

It's kind of like I want strum information available, but at macro time.

Is this a "common" problem that has a common answer?

I would probably separate the input method from the data structure using two traits, roughly like this:

/// This is the actual input method.
trait Prompt {
    /// Prompt for a value.
    ///
    /// Display `msg`, read a value, process it by `proc`, repeat until it returns `Ok(_)`
    fn prompt<F: Fn(&mut Self, &str) -> Result<R, String>, R>(&mut self, msg: &str, proc: F) -> R;
}

/// Promptable value.
trait Promptable {
    /// Use the `pr` to query user for values and produce `Self`
    fn prompt<P: Prompt>(pr: &mut P) -> Self;
}

Now you can implement an input method (in whatever crate you like) e.g.:

/// A simple prompter, just for demo
struct PrSimple;

impl Prompt for PrSimple {
    fn prompt<F: Fn(&mut Self, &str) -> Result<R, String>, R>(&mut self, msg: &str, proc: F) -> R {
        loop {
            let mut input_line = String::new();
            print!("{msg}: ");
            io::stdout().flush().expect("Failure flushing");
            io::stdin().read_line(&mut input_line).expect("Failed to read line");
            let input_line = input_line.trim();
            match proc(self, input_line) {
                Ok(value) => return value,
                Err(msg) => println!("{msg}"),
            }
        }
    }
}

And specify the structure of promptable data by implementing the Promptable trait on them:

impl Promptable for i32 {
    fn prompt<P: Prompt>(pr: &mut P) -> Self {
        pr.prompt("Enter a number", |_, input| {
            input.parse::<i32>().map_err(|_| "Invalid integer".to_string())
        })
    }
}

#[derive(Debug)]
// #[derive(Promptable)]
enum TheEnum {
    First,
    Second { value1: i32, value2: i32 },
}

// This would be generated by the `Promptable` derive macro
impl Promptable for TheEnum {
    fn prompt<P: Prompt>(pr: &mut P) -> Self {
        pr.prompt("Enter variant", |pr, input| {
            match input {
                "First" => Ok(TheEnum::First),
                "Second" => Ok(TheEnum::Second{ value1: i32::prompt(pr), value2: i32::prompt(pr) }),
                _ => Err("Wrong variant".to_string()),
            }
        })
    }
}

See, this implementation of Promptable does not depend on specific input method and only describes the structure of your data.

Playground