gojekfarm / xtools

XTools is a submodule based repo to host re-usable Golang code.
MIT License
5 stars 0 forks source link

xload: Struct level validation #42

Open ajatprabha opened 1 month ago

ajatprabha commented 1 month ago

Overview

With xload, I want to see what option(s) makes sense for doing validation post loading the values into struct and/or nested structs.

Example

```go type A struct { B *xloadtype.Listener `env:"B"` C *xloadtype.Listener `env:"C"` } func (a *A) Validate() error { if a.B == nil && a.C == nil { return errors.New("both A and B cannot be nil, one must be set") } } ``` The `required` keyword from xload isn't enough to handle such use-case, I think having an optional `Validator` interface implementation can be used to check if the object passes custom validation checks while returning up the call stack(after all nested values are loaded), and fail if the validation errors out. ```go type Validator interface { Validate() error } ```

Proposal

Add some mechanism to Validate structs at any level, and propagate errors all the way up, if any.

sonnes commented 1 month ago

Any kind of validation is already possible. Like tag based or function based can be done after loading the values.

What is the advantage of supporting validations inside xload?

ajatprabha commented 1 month ago

I agree, validations can be done after loading the config as well. Currently, doing manual validation in my code. However, that is explicit validation. Baking this as an optional flow into xload itself can offer implicit behaviour. And it scopes the validation into a well defined function similar to SetupSuite(), SetupTest() etc from github.com/stretchr/testify.

It keeps struct validation closer to struct itself and reduces cognitive load when dealing with multiple validations.

This requirement is mostly true for complex structs, made-up example:

//go:embed root.crt
var rootCert []byte

type Credentials struct {
    PrivateCert *x509.Certificate `vault:"ABC_CERT"`
}

func (c *Credentials) Validate() error {
    return c.PrivateCert.CheckSignatureFrom(rootCert)
}

I think traversal is a key part of why I would want this in xload and not outside, because xload visits every struct in the config, it is easier for it to call Validate if it has a method like that. Doing this manually will need careful thought and actually writing that logic. And, for repeating structs, I don't have to also add explicit validation in my code.

sonnes commented 1 month ago

Something like https://github.com/go-ozzo/ozzo-validation helps with defining and invoking validations, even for nested structs and complex multi value validation.

So the code will look like this:

cfg := Config{ }

err := xload.Load(...)

err = cfg.Validate()
sonnes commented 1 month ago

I’m inclined to not add invoking to xload because it would only work for single key value validation, but might be complex for multi value validation.

ajatprabha commented 1 month ago

Makes sense, let me give github.com/go-ozzo/ozzo-validation a go.

I’m inclined to not add invoking to xload because it would only work for single key value validation, but might be complex for multi value validation.

I don't get it, can you give an example?