ynqa / promkit

A toolkit for building interactive prompt in Rust
MIT License
237 stars 6 forks source link

Derive a form (multiple prompts) from struct #11

Open simonsan opened 3 months ago

simonsan commented 3 months ago

I would like to easily annotate a configuration struct, to ask the user for input for various fields and give a hint on validation:


use derive_bla::AskUser;

#[derive(Debug, AskUser)]
struct MyConf {
    #[ask(q="Please enter password:", hide_input=true, min_len=12)]
    password: Option<String>,

    #[ask(q="Please enter username:", min_len=3)]
    username: Option<String>,

    #[ask(q="Please enter age:", range=18..=85)]
    age: Option<u8>,
}

fn main() -> Result<()> {
    let mut conf = MyConf::default()
    conf.password()?.validate()?;
    conf.username()?.validate()?;
    conf.age()?.validate()?;

    println!("{conf#?}");
}

I would be interested if something like this is already existing, e.g. based on inquire, promkit, or if there is any interest to add this?

There are some ideas already in inquire, but it feels a bit out of scope.

CC: https://github.com/mikaelmello/inquire/issues/212 CC: https://github.com/mikaelmello/inquire/issues/65

EDIT: It would basically be the clap way of doing interactive prompts. Annotate something and give it to the user to let it be filled out. I think it comes with its own advantages and disadvantages, and excels a lot in a workflow where you suddenly want the user to fill out some data you already have bundled in a struct.

I think it could be a game changing feature, though. Because it would make dealing with interactive prompts much easier for a lot of users. Also, it would be excellent to have for people that don't want the full-fledged control of a ratatui TUI, but still a bit more ergonomic when it comes to interactivity and developer experience creating such.

ynqa commented 3 months ago

@simonsan Thank you for your proposal!

I would be interested if something like this is already existing, e.g. based on inquire, promkit, or if there is any interest to add this?

Currently, we do not offer such a feature (this is simply because I am not yet well-versed in Derive/macro). However, setting aside the specifics of the interface, I understand what you are looking to achieve and I am very interested in it.

simonsan commented 3 months ago

Nice! In one issue, there is an example/demo of how such a thing could work: https://github.com/IniterWorker/inquire_derive

Maybe it helps to get started. Also, there is the The Little Book of Rust Macros and there is the proc-macro workshop by dtolnay. I'm saying this, because I'm trying to support and hope that such a thing will exist at one point in time, as I think it would be really valuable to the ecosystem. (:

ynqa commented 3 months ago

@simonsan I've started by creating a prototype. I believe it needs to be refined further, but what do you think?

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Promkit;

#[derive(Default, Debug, Promkit)]
struct MyStruct {
    #[ask(
        prefix = "What is your name?",
        prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[ask(prefix = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let ret = MyStruct::default().ask_name()?.ask_age()?;
    dbg!(ret);
    Ok(())
}
simonsan commented 3 months ago

Looks really nice! 👍🏽 I need to try it out, to see if the API is ergonomic for me. But the way you instantiate and do it, is exactly how I would imagine it. In the end, it's like a builder pattern, that is being exposed directly to the user. For example, if you would like to ask for each field in the struct it would be pleasant to have a short calling method like MyStruct::ask().

Which would internally call Default::default() on all fields, if #[ask(default)] is set on this field.

Great work! 🌷

ynqa commented 3 months ago

@simonsan Thank you for your comment! 👍 Please feel free to continue posting your feedback after you have had a chance to try it out. As a favor, could I ask for the opportunity to have you take another review the macro after I've refined it based on your feedback?

simonsan commented 3 months ago

@simonsan Thank you for your comment! 👍 Please feel free to continue post your feedback after you have had a chance to try it out. As a favor, could I ask for the opportunity to have you take another look at the macro after I've refined it based on your feedback?

For sure! :) 👍🏽

ynqa commented 3 months ago

@simonsan I refined it in PR #17. Here are the changes:

The concerns are as follows, and I would appreciate your opinions:

Example:

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Promkit;

#[derive(Default, Debug, Promkit)]
struct Profile {
    #[readline(
        prefix = "What is your name?",
        prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[readline(default)]
    hobby: Option<String>,

    #[readline(prefix = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let mut ret = Profile::default();
    ret.readline_name()?;
    ret.readline_hobby()?;
    ret.readline_age()?;
    dbg!(ret);
    Ok(())
}

Thanks 👍

simonsan commented 3 months ago

I'm on the way to going out, so only tiny feedback:

    /// Tags for the activity
    #[cfg_attr(
        feature = "clap",
        clap(
            short,
            long,
            group = "adjust",
            name = "Tags",
            value_name = "Tags",
            visible_alias = "tag",
            value_delimiter = ','
        )
    )]
    tags: Option<Vec<String>>,
simonsan commented 3 months ago

Some naming ideas:

For the readline attribute:

For the prefix attribute:

ynqa commented 3 months ago

@simonsan Thanks for your comments! 👍

Indeed, the terminology needs to be clearer about what can be done. It felt like it could also be offered as something separate from presets.

Accepting collections with delimiters is a good idea. On the other hand, incorporating various parser logics might make the macro complex (it already seemed quite complex when allowing for Option). Therefore, what do you think about accepting parser functions in the attribute for types beyond primitives, like str -> Vec<T>, str -> Option<T> (I believe this was also in clap)?

simonsan commented 3 months ago

Some idea:

use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
use promkit_derive::Form;

#[derive(Default, Debug, Form)]
struct Profile {
    #[input(
        prompt = "What is your name?",
        prompt_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
    )]
    name: String,

    #[input(default)]
    hobby: Option<String>,

    #[input(prompt = "How old are you?", ignore_invalid_attr = "nothing")]
    age: usize,
}

fn main() -> Result {
    let mut ret = Profile::default();
    ret.prompt_name()?;
    ret.prompt_hobby()?;
    ret.prompt_age()?;
    dbg!(ret);
    Ok(())
}

FormBuilder could be also a viable option, as in:

use promkit_derive::FormBuilder;

#[derive(Default, Debug, FormBuilder)]
simonsan commented 3 months ago

A form could be even layouted like this:

Please fill out the form:
------------------------
What is your name?: John Doe
What is your hobby? (Leave empty if none):
How old are you?: 25

Use Up/Down to navigate through the inputs
Press Enter if you are ready to continue

I don't know if that is easily possible, though 😅 It's not a dealbreaker, would just double down on the Form

simonsan commented 3 months ago

Accepting collections with delimiters is a good idea. On the other hand, incorporating various parser logics might make the macro complex (it already seemed quite complex when allowing for Option). Therefore, what do you think about accepting parser functions in the attribute for types beyond primitives, like str -> Vec<T>, str -> Option<T> (I believe this was also in clap)?

I agree completely. We don't want to make it too complex. I think clap has https://docs.rs/clap/latest/clap/builder/struct.ValueParser.html for this.