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:
- General code-review feedback, if you have any comments on the generated code, overall idea, or API.
- 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?