Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Ability to get Option<T> or Option<String> input #287

Open
tgrushka opened this issue Jan 7, 2025 · 0 comments
Open

Feat: Ability to get Option<T> or Option<String> input #287

tgrushka opened this issue Jan 7, 2025 · 0 comments

Comments

@tgrushka
Copy link

tgrushka commented Jan 7, 2025

Is your feature request related to a problem? Please describe.
Often, user input can be Optional, and an empty String or T::default() value is not desirable.

Describe the solution you'd like
Allow Text and CustomType inputs to return Option<String> and Option<T> -- or implement OptionText / OptionType structs (because the trait bound Option<f64>: std::str::FromStr, etc. is not satisfied, etc., which may be non-trivial to work around) -- that return None if the field is left blank or an optional "default" or "none" value is provided.

Describe alternatives you've considered
Here are a couple illustrative wrappers I just hacked together that are not feature-complete. The drawback to these is the lack of ability to chain the .with_ methods as in the API -- requiring arguments or a struct argument. Would be nice to have the Option<T> ability "built-in" to the crate.

EDIT: It's complicated to handle None values + all the other options and get it to work right.

use std::{fmt::Display, str::FromStr};

use anyhow::Result;
use inquire::{CustomType, formatter::StringFormatter, validator::StringValidator};

/// additional_none_values: `&str` values (in addition to an empty string) that are treated as None.
pub fn optional_text(
    prompt: &str,
    current: &Option<String>,
    additional_none_values: &[&str],
    validators: &[Box<dyn StringValidator>],
    formatter: Option<StringFormatter<'_>>,
) -> Result<Option<String>> {
    let mut input = inquire::Text::new(prompt).with_validators(validators);
    if let Some(current) = current {
        input = input.with_default(current);
    };
    if let Some(formatter) = formatter {
        input = input.with_formatter(formatter);
    }

    let none_values = additional_none_values
        .iter()
        .map(|v| v.to_lowercase())
        .chain(["null".to_string(), "<none>".to_string(), "none".to_string()])
        .collect::<Vec<_>>();

    let formatter = &|s: &str| {
        if s.is_empty() || none_values.contains(&s.to_lowercase()) {
            "<None>".to_string()
        } else {
            s.to_string()
        }
    };

    input = input.with_formatter(formatter);

    let text = input.prompt()?.trim().to_string();

    if text.is_empty() || none_values.contains(&text.to_lowercase()) {
        Ok(None)
    } else {
        Ok(Some(text))
    }
}

/// `none_value`: The value of T that represents None. If None, `T::default()` is used.
/// additional_none_values: `&str` values that are converted to `none_value` by the parser.
pub fn optional_custom_input<T>(
    prompt: &str,
    current: &Option<T>,
    none_value: Option<&T>,
    additional_none_values: &[&str],
) -> Result<Option<T>>
where
    T: Default + PartialEq + Clone + FromStr + Display,
{
    let none_value = match none_value {
        Some(none_value) => none_value,
        None => &T::default(),
    };

    let formatter = &|v: T| {
        if v == *none_value {
            "<None>".to_string()
        } else {
            v.to_string()
        }
    };

    let none_values = additional_none_values
        .iter()
        .map(|v| v.to_lowercase())
        .chain(["null".to_string(), "<none>".to_string(), "none".to_string()])
        .collect::<Vec<_>>();

    let value = CustomType::<T>::new(prompt)
        .with_default(current.clone().unwrap_or(none_value.clone()))
        .with_formatter(formatter)
        .with_default_value_formatter(formatter)
        .with_parser(&|s| {
            let s = s.trim();
            if s.is_empty() || none_values.contains(&s.to_lowercase()) {
                Ok(none_value.clone())
            } else {
                s.parse().map_err(|_| ())
            }
        })
        .prompt()?;

    if value == *none_value {
        Ok(None)
    } else {
        Ok(Some(value))
    }
}

Additional context
I assume this section is Optional. 🤣

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant