garde icon indicating copy to clipboard operation
garde copied to clipboard

Serialize constraints

Open jprochazk opened this issue 2 years ago • 3 comments

It would be useful to support getting a list of constraints on a struct and serializing them to JSON. A possible use-case is sharing the validations between the backend and frontend of a web app without having to manually keep them in sync.

#[derive(Validate)]
struct User {
  #[garde(required)]
  first_name: String,
  #[garde(required)]
  last_name: String,
  #[garde(length(max=100))]
  bio: String,
}

let constraints = <User as garde::Constraints>::constraints();

let serialized = serde_json::to_string(&constraints).unwrap();
{
  "first_name": [{"type":"required"}],
  "last_name": [{"type":"required"}],
  "bio": [{"type":"length","params":{"max":100}}]
}

Error messages should also support serde::Serialize. Some inspiration could be taken from similar libraries in other ecosystems, such as zod for TypeScript.

The output should respect rename.

jprochazk avatar Mar 26 '23 10:03 jprochazk

Serializing error reports was implemented in https://github.com/jprochazk/garde/commit/a989bcbfd28a4d7aa2c79ac936ee5c19ac857f25. All that's missing is serializing constraints, which shouldn't be too difficult to implement:

  • It should be emitted as part of the Validate derive
  • It should be a straightforward translation of the model.

For example:

#[derive(garde::Validate)]
struct Foo {
    #[garde(length(min = 1))]
    s: String,
}

Would derive the following:

impl garde::Validate for Foo {
    // ... the usual validate impl
  
    fn constraints() -> ::garde::Constraints {
        use ::garde::constraints::*;
        
        Constraints::Struct(
            Struct {
                fields: vec![
                    (
                        "s".into(),
                        vec![
                            Rule::Length {
                                min: Some(1),
                                max: None
                            }
                        ],
                    )
                ]
            }
        )
    }
}

jprochazk avatar Jan 08 '24 23:01 jprochazk

Thinking about this a bit more. Our support for arbitrary expr in certain rules throws a wrench into this being a fully compile-time thing.

If you have e.g. length(min=1), then that's a constant and we can embed it directly into the resulting serialized constraints. But given something like length(min=ctx.min), it's not clear how you'd serialize the ctx.min value if you don't have access to it without the user providing a context. Worse yet is length(min=self.min), which depends on the contents of self.

jprochazk avatar Jul 03 '24 17:07 jprochazk

Hi, just using this crate for the first time today. I'm making an app which uses 'egui_i18n' and 'egui_form' with the 'garde' feature. Currently, when a validation error occurs the message is returned in English, however that's no good when you're making an app that needs to be in multiple languages.

egui_i18n uses the popular 'fluent-rs' ( crate for translations, when rendering error messages you need two things: 1 - the fluent translation key, 2 the arguments. in the case of form validation errors the arguments are the parameters on the garde derive constraints.

example struct:

struct MyForm {
    #[garde(length(min = 1))]
    name: String,
}

example translation file:

my-form-name = Name
my-form-name-error-length = Must be longer than { $min } characters.

And the $min fluent arg would come from the derive constraints.

However, there's another issue because 'max' is not specified in the example above, if it was you'd need both the $min and $max constraints and also use a different translation, as below:

example struct:

struct MyForm {
    #[garde(length(min = 1))]
    value1: String,
    #[garde(length(max = 10))]
    value2: String,
    #[garde(length(min = 1, max = 10))]
    value3: String,
}

example translation file:

form-field-error-length-too-short = Must be longer than { $min } characters.
form-field-error-length-too-long = Must be shorter than { $max } characters.
form-field-error-length-incorrect = Must be between { $min } and { $max } characters.

this presents an issue, how does the developer select the right translation key?

proposal - specification of the a key for each rule:

struct MyForm {
    #[garde(length(min = 1, key = "form-field-error-length-too-short"))]
    value1: String,
    #[garde(length(max = 10, key = "form-field-error-length-too-long"))]
    value2: String,
    #[garde(length(min = 1, max = 10, key = "form-field-error-length-incorrect"))]
    value3: String,
}

The key and the arguments could then be programmatically used to pick the right translation key and to supply the arguments to it.

hydra avatar Feb 07 '25 15:02 hydra