murarth / gumdrop

Rust option parser with custom derive support

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add a 'default' option

RazrFalcon opened this issue · comments

Add support for a default value.

#[options(help = "Sets the target DPI", meta = "DPI", default = "96")]
dpi: u32,

If it possible it would be great to set a type instead of a string as a default value.

The code generated by gumdrop uses the standard Default trait to generate the default values of all options.

To specify a custom default value for only one field, you have a few options:

  • Manually implement Default for your type, supplying Default::default() for every other field. This is, of course, understandably, tedious for a large struct.
  • Use the smart-default crate to generate a Default implementation, with specific field values. It looks as though the code hasn't been updated in a while, so I'm not sure if it still works with the latest version of Rust.
  • Use a custom type with a manual Default implementation (in conjunction with a manual FromStr implementation, as outlined in #13), e.g.:
#[derive(Default, Options)]
struct Options {
    dpi: Dpi,

    // ...
}

struct Dpi(u32);

impl Default for Dpi {
    fn default() -> Dpi {
        Dpi(96)
    }
}

I understand your intentions, but it's tedious for simple numeric values. Which is the most common use case.

It may be tedious, but that is not a problem unique to gumdrop and I don't feel that this would be the right way to address it.

The current functionality is that default values come from the Default trait, plain and simple. Providing that Default implementation in the manner most convenient to the programmer is an issue, I feel, that is better solved outside the gumdrop crate itself.

If you have a number of options with default values, the smart-default crate would appear to be worthwhile for your use case. If not, perhaps an Option<u32> field and Option::unwrap_or solves the problem neatly. There are options to address this issue and they are not so cumbersome, I think, to warrant implementing a new feature in gumdrop.

Well, structopt support this and it's the main rival.

On the other hand, implementing a dedicated struct for each input type maybe not that bad an idea. Will try it on the real code.

But docs and examples should explain this anyway.

Thanks for detailed answers.

Tried it out and it doesn't work. Here is the real world example: I have an app that outputs an XML data. It has two similar options: nodes indent and attributes indent. They both support the same input values, but the default one are different. To implement this I have to create two Indent structs, which bloats the code a lot.

Here is the minimal code I came up with:

fn parse_indent(s: &str) -> Result<svgdom::Indent, &'static str> {
    let indent = match s {
        "none" => svgdom::Indent::None,
        "0" => svgdom::Indent::Spaces(0),
        "1" => svgdom::Indent::Spaces(1),
        "2" => svgdom::Indent::Spaces(2),
        "3" => svgdom::Indent::Spaces(3),
        "4" => svgdom::Indent::Spaces(4),
        "tabs" => svgdom::Indent::Tabs,
        _ => return Err("invalid INDENT value"),
    };

    Ok(indent)
}

#[derive(Debug)]
struct Indent(svgdom::Indent);

impl Default for Indent {
    fn default() -> Self {
        Indent(svgdom::Indent::Spaces(4))
    }
}

impl FromStr for Indent {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_indent(s).map(Indent)
    }
}


#[derive(Debug)]
struct AttrsIndent(svgdom::Indent);

impl Default for AttrsIndent {
    fn default() -> Self {
        AttrsIndent(svgdom::Indent::None)
    }
}

impl FromStr for AttrsIndent {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_indent(s).map(AttrsIndent)
    }
}

And it can be simplified to just:

#[derive(Debug)]
struct Indent(svgdom::Indent);

impl FromStr for Indent {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let indent = match s {
            "none" => svgdom::Indent::None,
            "0" => svgdom::Indent::Spaces(0),
            "1" => svgdom::Indent::Spaces(1),
            "2" => svgdom::Indent::Spaces(2),
            "3" => svgdom::Indent::Spaces(3),
            "4" => svgdom::Indent::Spaces(4),
            "tabs" => svgdom::Indent::Tabs,
            _ => return Err("invalid INDENT value"),
        };

        Ok(Indent(indent))
    }
}

And even more with fn pointer to validator:

fn parse_indent(s: &str) -> Result<svgdom::Indent, &'static str> {
    let indent = match s {
        "none" => svgdom::Indent::None,
        "0" => svgdom::Indent::Spaces(0),
        "1" => svgdom::Indent::Spaces(1),
        "2" => svgdom::Indent::Spaces(2),
        "3" => svgdom::Indent::Spaces(3),
        "4" => svgdom::Indent::Spaces(4),
        "tabs" => svgdom::Indent::Tabs,
        _ => return Err("invalid INDENT value"),
    };

    Ok(indent)
}

#[derive(Debug, Default, Options)]
struct Args {
    #[options(no_short, help = "Sets the XML nodes indent", meta = "INDENT", default = "4", validate(parse_indent))]
    indent: svgdom::Indent,

    #[options(no_short, help = "Sets the XML attributes indent", meta = "INDENT", default = "none", validate(parse_indent))]
    attrs_indent: svgdom::Indent,

    #[options(free)]
    free: Vec<String>,
}

And more importantly, this is already supported by structopt, but the reason I'm looking for an alternatives is the clap overhead. It makes the executable ~300KiB bigger, while your library is just ~10KiB. Just like getopts, but it doesn't support derive.

Do'h! I have totally forgot about the unwrap_or trick. This helps a lot. But I still don't like the idea that default value is set outside the Options struct.

The result code is here. Any suggestions are welcome.

Yes, unwrap_or does have the drawback of separating the default value from the option declarations. However, using the smart-default crate does not have this drawback. It simply requires another attribute element for some fields. Would this work for your use case?

Would this work for your use case?

No, because I don't have a default value. Different args will have a different default value for the same type.

I think perhaps you're not understanding me. The smart-default crate allows specifying a default value for each field. You can use this to specify a default value independent of a field's type. Using it with gumdrop might look something like this:

#[derive(Options, SmartDefault)]
struct Foo {
    #[options(help = "help text", meta = "FOO")]
    #[default = "123"]
    foo: u32,
}

Oh... I see. But using an additional crate for such a simple/obvious/common task seems like an unnecessary complication.

I've moved to a custom Default implementation and it made the code much simpler. But still, custom wrapper types with FromStr creating an unnecessary noise.

Okay, I gave this some more thought and I think you're right.

Allowing a per-field default value does not fundamentally conflict with gumdrop's core operations. In addition to easing your use case, such functionality also facilitates the general case: users don't need to add Default to the derive list to get basic functionality because the generated code will instead use Default::default() for each field that isn't provided an explicit default value.

I've just pushed a commit that makes this change and adds the default attribute. If it behaves as expected and you're satisfied with it, you can close this issue.

(And I know I'm stubborn and resistant to change, so thanks for being persistent about this.)

Thanks a lot! It's almost perfect for my use cases now.

AFAIU, the default option requires a value type, not a string (like in structopt). Is this intentional? I've expected that default will parse the provided string.

I understand that parsing implies some overhead, but it's how structopt work and it will simplify the transition. Also, in my case I have to write something like svgdom::Indent::Spaces(4) instead of 4. I think that the default value should represent the string value we can get from args. Especially because it is in double quotes, which implies a string. In that way, it can also be used during the usage generation (which is how structopt/clap works).

(I hope that I'm not pushing too far.)

It was not a deliberate design decision. Just the first implementation strategy that came to mind. Parsing it as a string does make more sense. Unfortunately, it does mean that an invalid default value string will be a runtime error instead of a compile-time error. But that isn't too big a deal.

I've pushed a commit making the above change and another adding default value to generated usage text.

(Don't worry. You're fine. It's great to receive input on improving the crate.)

Thanks a lot! It works as expected now.