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

Move ContextKind and ContextValue into one enum #5683

Open
2 tasks done
TheTollingBell opened this issue Aug 18, 2024 · 12 comments
Open
2 tasks done

Move ContextKind and ContextValue into one enum #5683

TheTollingBell opened this issue Aug 18, 2024 · 12 comments
Labels
A-help Area: documentation, including docs.rs, readme, examples, etc... C-enhancement Category: Raise on the bar on expectations M-breaking-change Meta: Implementing or merging this will introduce a breaking change. S-waiting-on-decision Status: Waiting on a go/no-go before implementing

Comments

@TheTollingBell
Copy link

TheTollingBell commented Aug 18, 2024

Please complete the following tasks

Clap Version

master

Describe your use case

When looking at the clap error formatters there are lots of non-rusty things such as multiple if let and non-exhaustive enum matching when dealing with contexts:

let suggestions = error.get(ContextKind::Suggested);
if let Some(ContextValue::StyledStrs(suggestions)) = suggestions {

Not only is this code messy, it also makes using contexts generally annoying for an end user without digging through the code to see which contexts have been implemented and what kinds of values they can take.

Describe the solution you'd like

This could be done better by merging ContextKind and ContextValue into one enum as well as switching the code in existing formatters to use match instead of Error.get(). (Unless there is some reason that it is being used as such.)

Enforcing that certain ContextKind be restricted to only certain values would make the context API more accessible as well as allowing a singular match instead of multiple if statements in formatters. This can be done with a single Context enum that contains the ContextKind and multiple nested <context type>ContextValue enums.

Alternatives, if applicable

No response

Additional Context

No response

@TheTollingBell TheTollingBell added the C-enhancement Category: Raise on the bar on expectations label Aug 18, 2024
@epage epage added M-breaking-change Meta: Implementing or merging this will introduce a breaking change. A-help Area: documentation, including docs.rs, readme, examples, etc... S-waiting-on-decision Status: Waiting on a go/no-go before implementing labels Aug 19, 2024
@epage
Copy link
Member

epage commented Aug 19, 2024

it also makes using contexts generally annoying for an end user without digging through the code to see which contexts have been implemented and what kinds of values they can take.

Are you referring to a person creating an error or rendering it? Some of your comments make it sound like both perspectives and I'm wanting to better understand your use case. For whichever side you are on, what problem are you solving by using this API?

Enforcing that certain ContextKind be restricted to only certain values would make the context API more accessible as well as allowing a singular match instead of multiple if statements in formatters.

Something to consider is how far do we go on being more statically typed. The Context is dependent on the ErrorKind, so we could make an Error enum with variants that have variants of context which have variants of values. That is a large explosion of types that require hand implementing every combination of variants without the ability to reuse code for the default implementation (which runs counter to our goals of reducing binary size and compile times). Due to those goals, we also wanted to allow people to have an even cheaper implementation of error reporting that renders as key-value pairs. Admittedly, people have likely not taken us up on that.

This API also gives us a lot more flexibility to evolve things without committing to any specific details, allowing us to gracefully degrade.

Originally, our error type was not open to extension for content or rendering. Opening it up was to offer some extra flexibility for more advanced cases and is lower in our design priority list compared to other values.

@epage
Copy link
Member

epage commented Aug 19, 2024

As for some of the details...

non-exhaustive enum matching

With your proposed API, users rendering their own errors would still need to be non-exhaustive because we'd use #[non_exhaustive] to allow evolving the API.

s switching the code in existing formatters to use match instead of Error.get(). (Unless there is some reason that it is being used as such.)

Error is presented as a container of context, with the Kinds being unique keys. This becomes a bit messier to enforce uniqueness with a single Context.

@TheTollingBell
Copy link
Author

TheTollingBell commented Aug 19, 2024

Are you referring to a person creating an error or rendering it?

I'm kind of referring to both sides, not only were contexts not well documented for me as an end-user (what was and wasn't supported by each formatter). It was annoying to improve it because it was hard to communicate to a potential end-user what should and shouldn't be used. If not this, then at least provide documentation over either the formatter implementations or the ContextKind/ContextValue enums so it is clear what is and isn't implemented.

large explosion of types that require hand implementing every combination of variants without the ability to reuse code for the default implementation

While this concern is valid, as it stands the context API is relatively hard to use and unspecific. Perhaps a feature-gated context! macro which would allow compile time errors if an invalid combination of ContextKind/ContextValue are given?

@epage
Copy link
Member

epage commented Aug 20, 2024

I'm kind of referring to both sides, not only were contexts not well documented for me as an end-user (what was and wasn't supported by each formatter).

Sounds like your primary interest is in generating errors and in support of improving that, the complexity of implementing a formatter was a concern.

For generating errors, could you expand on why you are doing so? What problems are you solving within your application? Understanding use cases is important for knowing what to improve, how to improve it, and how urgent improving it is.

@TheTollingBell
Copy link
Author

TheTollingBell commented Aug 20, 2024

For generating errors, could you expand on why you are doing so?

I was trying to write a TypedValueParser for a type that could be parsed directly from a string. I used NonEmptyStringValueParser to parse to a String which I then converted to the type. When I saw the context API I thought it would be useful in order to include extra data about what step of parsing went wrong but in whatever "standardized" way clap thought I should.

Maybe the above usage is a misuse, but it seemed simpler and more lightweight to use the auto-generated messages instead of something custom. To add to the confusion, the example given here uses ContextKind::InvalidArg which doesn't even show anything when using the default formatter.

@epage
Copy link
Member

epage commented Aug 21, 2024

I was trying to write a TypedValueParser for a type that could be parsed directly from a string. I used NonEmptyStringValueParser to parse to a String which I then converted to the type. When I saw the context API I thought it would be useful in order to include extra data about what step of parsing went wrong but in whatever "standardized" way clap thought I should.

Thanks for providing this context!

You can do NonEmptyStringValueParser::new().try_map(...) to report your own error and have it wrapped in all of the clap error report trappings. What "special" reporting were you looking at doing on top of what this provides?

@TheTollingBell
Copy link
Author

TheTollingBell commented Aug 21, 2024

What "special" reporting were you looking at doing on top of what this provides?

try_map() is great but providing context like alternative arguments as well as formatting an error in the correct style for a formatter makes the context API feel much more polished than just dumping an unformatted string into an Err.

@epage
Copy link
Member

epage commented Aug 22, 2024

Could you provide concrete examples of what you are trying to get out of context, of that special reporting? Its helpful to understand the specific problems you are running into to make sure we are on the same page and can consider the right set of solutions, rather than solutions for a situation I guessed that isn't what you are doing.

For example, looking over format.rs, I don't see a reason to use it. If I were, the things that seem the most relevant are

  • ContextKind::Suggested*
  • Showing possible values or subcommands
  • Adding in the coloring
  • Staying character for character consistent with clap

@TheTollingBell
Copy link
Author

impl TypedValueParser for MonitorParser {
    type Value = &'static Monitor;

    fn parse_ref(
        &self,
        cmd: &Command,
        arg: Option<&Arg>,
        value: &OsStr,
    ) -> Result<Self::Value, Error> {
        // guaranteed to be non-empty by parser
        let value = NonEmptyStringValueParser::new().parse_ref(cmd, arg, value)?;
        let monitors = get_monitors();

        match monitors {
            Ok(monitors) => monitors.iter().find(|v| v.name == value).ok_or(
                Error::new(ErrorKind::ValueValidation)
                    .with_context(ContextKind::InvalidArg, ContextValue::String(value))
                    .with_context(
                        ContextKind::ValidValue,
                        ContextValue::Strings(monitors.iter().map(|v| v.name.clone()).collect()),
                    )
                    .with_cmd(cmd),
            ),

            Err(e) => Err(Error::new(ErrorKind::ValueValidation)
                .with_context(
                    ContextKind::Custom,
                    ContextValue::String(format!("Error whilst fetching monitors: {e}")),
                )
                .with_cmd(cmd)),
        }
    }
}

Expected Result:

Some kind of output saying Value Validation failed alongside all of the contexts given.

Actual Result:

error: invalid value for one of the arguments

For more information, try '--help'.

@epage
Copy link
Member

epage commented Aug 27, 2024

As a heads up, I was primarily looking for your "Expected Result" which you left out.

I'm assuming you are looking for

error: invalid value 'slo' for '-O <option>'
  [possible values: slow, fast, \"ludicrous speed\"]

  tip: a similar value exists: 'slow'

For more information, try '--help'.

(with arguments/values actually related to your code)

I'm assuming the use of InvalidArg when it should be InvalidValue is an example of why you are looking for things to be more strictly defined.

I'm also assuming the reason the possible values aren't defined ahead of time is that its a slow, failable operation.

A workaround would be to have your TypedValueParser create a PossibleValuesParser on demand to report everything.

This type of pattern seems reasonable enough that I wonder if we should have built-in support, e.g. PossibleValuesParser could accept a closure of possible values. While it doesn't solve the bigger picture, it makes reasonably expected cases much more ergonomic than a big picture solution.

@TheTollingBell
Copy link
Author

TheTollingBell commented Aug 27, 2024

I think that this a great compromise between an in-depth system and nothing. I would be happy to write a PR adding a LazyPossibleValuesParser that would allow the passing of a closure that returns all possible values. The only question I have is handling Result types from the closure.

Most likely same technique used TypedValueParser::try_map()?

@epage
Copy link
Member

epage commented Aug 28, 2024

Yes, feel free to post a PR for LazyPossibleValuesParser and to handle errors like try_map.

I was originally hoping to have this built-in to PossibleValuesParser but that would require boxing the Fn which would require being more strict on the error type as well as other compromises. In a future release we could merge the two but I think there would be problems with making the constructor for LazyPossibleValuesParser that accepts possible values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-help Area: documentation, including docs.rs, readme, examples, etc... C-enhancement Category: Raise on the bar on expectations M-breaking-change Meta: Implementing or merging this will introduce a breaking change. S-waiting-on-decision Status: Waiting on a go/no-go before implementing
Projects
None yet
Development

No branches or pull requests

2 participants