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

Custom error messages and localization #7

Open
jprochazk opened this issue Mar 26, 2023 · 11 comments
Open

Custom error messages and localization #7

jprochazk opened this issue Mar 26, 2023 · 11 comments
Labels
enhancement New feature or request
Milestone

Comments

@jprochazk
Copy link
Owner

jprochazk commented Mar 26, 2023

A rule for custom error messages:

#[derive(Validate)]
struct Test {
    #[garde(required, message="custom message which is actually a format string: {}")]
    a: String,
    #[garde(required, message=custom_message_format)]
    b: String,
    #[garde(required, message=|_: &str, value: &str, _: &()| {
        Ok(format!("custom message in closure: {v}"))
    })]
    c: String,
}

fn custom_message_format(field_name: &str, value: &str, context: &()) -> Result<Cow<'static, str>, Error> {
    Ok(format!("custom message: {value}"))
}

Implementation detail: The string version should differentiate between a const string and a format string. It might make sense to generate a temporary function for format strings which does the actual formatting to enforce encapsulation.

Combined with a custom Context, it should be possible to use this to localize error messages:

struct Locale {
  /*
  - current locale 
  - mapping of (unlocalized -> localized) strings
  */
}

#[derive(Validate)]
#[garde(context(Locale))]
struct User {
  #[garde(length(min=10,max=100), message=|_, v: &str, ctx: &Locale| {
    translate!(ctx, "invalid value {v}")
  })]
  value: String,
}

But it might make sense to support this directly via a translate rule which passes the error message to be further localized - needs some design work.

@jprochazk
Copy link
Owner Author

jprochazk commented Aug 12, 2023

There are at least two issues which I believe to be pre-requisites:

  1. Fail-fast validation #1
  2. Improve error representation #64

(1) because message should act as a switch that turns on fail-fast validation for that specific field, so that we don't waste resources constructing errors that will be discarded.

(2) because the current error representation is somewhat inflexible, and adding a custom message would mean emitting Simple(vec![Error::new("message")]) for a field with a custom message. That's a vec allocation just for one error 😢. With a flat representation (described in the issue), this would be cheap.

These two issues would require pretty significant changes in both garde_derive and garde.

@jprochazk jprochazk modified the milestones: v1.0.0, v0.15 Sep 2, 2023
@jprochazk
Copy link
Owner Author

jprochazk commented Sep 3, 2023

I think this and #1 can be combined into a single design. There are two use cases for "custom messages":

  1. Append the custom message as an extra error if any validation fails.
  2. Replace all errors with just the custom message if any validation fails.

(1) would address #63, because you could use this to output the value together with the other errors.

(2) could address #1. We would special case the output of the proc macro to not actually produce any errors in this case, and immediately stop all further validation when any of the rules fail. This is fail-fast validation. We could then support using the custom message attribute on both field and struct levels. By using a "Replace" custom message on the struct level, you would have fail-fast validation for the entire struct.

This would require reworking all the rules to support two modes:

  • Check only
  • Check and emit error

So that we can fully skip producing an error in case we don't need it.

The proc macro will also need to support emitting the field-level validation rules in both fail-fast and error aggregate modes.

One limitation is that this would all be encoded in the derived Validate implementation, there would be no way to "switch" between the two for a single type. This means if you wanted to do "fail-fast" validation, and then fall back to full validation with error aggregation, you would need two types. I think that's fine, because you can just generate the two from a single type using a declarative macro.

@jprochazk jprochazk modified the milestones: v0.15, v0.16 Sep 9, 2023
@mustafasegf
Copy link

i would love to have this feature. Right now we ended up using custom validator to make custom error message. If I want to help is there's anything that I could do?

@jprochazk
Copy link
Owner Author

is there's anything that I could do?

I think adding a comment here with a code snippet showing how you'd use this feature would be super helpful!

Depending on what exactly you need out of this feature, there may be a simpler "MVP" of #7 (comment) that would not be too difficult to implement. It would consist of:

  • Adding a message rule to accept input as described in the original post. (It could start with only accepting a format string)
  • Emitting the message into the error report if any other validation on the field failed.

@lasantosr
Copy link
Contributor

I'm planning to migrate from validator but I'm missing the code and params on the Error struct.

I'd like to apply i18n on the client side, based on pre-defined error codes that maps to messages with interpolated parameters, like max and min.

For example this is how I'm returning error codes to clients: https://github.com/lasantosr/error-info

@jprochazk
Copy link
Owner Author

I want to avoid adding params. Correct me if I'm wrong, but for your use case, wouldn't code alone be enough? You should be able to retrieve the internationalized error message template using code as the key, and then interpolate the value retrieved from the field on the client.

As for adding support for code, I opened an issue to track it here:

@omid
Copy link

omid commented Mar 30, 2024

@lasantosr I have it like this, with code, message and path:

impl From<garde::Report> for ApiError {
    /**
    Recursively generate a vector of "ApiErrorBase"s
    and fill it with dot-separated "path"es and error messages
    */
    fn from(e: garde::Report) -> Self {
        e.iter()
            .map(|(key, error)| ApiErrorBase::new_with_path(&format!("{key}.invalid"), &error.message().to_string().to_sentence_case(), &format!("{key}")))
            .collect_vec()
            .into()
    }
}

And the method is like so:

    #[must_use]
    pub fn new_with_path(code: &str, message: &str, path: &str) -> Self {
        Self {
            code: code.to_string(),
            message: message.to_string(),
            path: Some(path.to_string()),
            params: None,
        }
    }

I hope it satisfies your need.

@lasantosr
Copy link
Contributor

I want to avoid adding params. Correct me if I'm wrong, but for your use case, wouldn't code alone be enough?

That's a first step, the problem is if the i18n message associated to the code is something like: "The value must not exceed {max}"

The params are needed in those cases, but I agree that's an overhead not required for every use case, so it can be an opt-in parameter on the struct level macro.

@lasantosr I have it like this, with code, message and path

A field might have different validation rules associated, so I would like to have a different code for each. Instead of a generic "username.invalid" I'd like to have "username.invalid_length" and "username.invalid_characters" for example.

@omid
Copy link

omid commented Mar 30, 2024

A field might have different validation rules associated, so I would like to have a different code for each. Instead of a generic "username.invalid" I'd like to have "username.invalid_length" and "username.invalid_characters" for example.

So it can be two extra properties in the Error. Since the checks are defined, they can have static values. So for example this code will be something like this by default:

Err(Error::new("Length is lower than {}", vec![min.to_string()], "min_length"))

or

Err(Error::new("Length is lower than {min}", HashMap::from([("min", min.to_string())]), "min_length"))

Which can be customized via attributes.

@jprochazk
Copy link
Owner Author

That's a first step, the problem is if the i18n message associated to the code is something like: "The value must not exceed {max}"

The params are needed in those cases, but I agree that's an overhead not required for every use case, so it can be an opt-in parameter on the struct level macro

This should be addressed by:

@lasantosr
Copy link
Contributor

Totally missed that one! That would be enough, along with this message customization and the custom error codes

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

No branches or pull requests

4 participants