elastio / bon

Generate builders for everything!
https://elastio.github.io/bon/
Apache License 2.0
967 stars 16 forks source link

Input-validating builders #34

Open mirosval opened 1 month ago

mirosval commented 1 month ago

Hello and thank you for writing this lib!

I'm wondering if extending this lib with validation would be in scope. What I'm imagining is an interface where I can specify input validators (e.g. string -> uuid). The builder would then run them all and collect the errors. The type of the build method would be fn build() -> Result<T, Vec<E>> or something like this.

It is a common scenario in a lot of software to write types that have some guaranteed invariants, but are built from some basic wire types. Validating all of the fields manually is a lot of boilerplate. I've attempted to do a PoC of this in my crate called valibuk, but I don't have the capacity to properly maintain an OSS project.

A note for the community from the maintainers

Please vote on this issue by adding a 👍 reaction to help the maintainers with prioritizing it. You may add a comment describing your real use case related to this issue for us to better understand the problem domain.

Veetaha commented 1 month ago

Hi, thank you for opening the issue! I can see this within the scope of bon. I understand you'd like some short syntax for validating the inputs and collecting a Vec of errors. This requires some design for the attributes interface for such a feature. I've seen some other crates that can do validation and your PoC will also be a good reference for this feature. It'll require some work, but we can try to get there, although it won't be an immediate priority for bon.

Right now, with the current state of bon it's possible to do validation, but it requires writing a bit more code by using #[builder] on the type's new() method. It allows you to define any custom logic that you'd like inside of the new() method. The builder will use your implementation in the build() method:

use anyhow::{Context, Error};
use bon::bon;
use uuid::Uuid;

struct User {
    name: String,
    id: Uuid,
    group_id: Uuid,
}

#[bon]
impl User {
    #[builder]
    fn new(name: String, id: &str, group_id: &str) -> Result<Self, Vec<Error>> {
        let mut errors = vec![];

        if name.contains("@#!$") {
            errors.push(anyhow::anyhow!("Name contains invalid characters: {name}"));
        }

        let id = Uuid::parse_str(id)
            .with_context(|| format!("Invalid UUID: {id}"))
            .map_err(|err| errors.push(err));

        let group_id = Uuid::parse_str(group_id)
            .with_context(|| format!("Invalid UUID: {group_id}"))
            .map_err(|err| errors.push(err));

        if !errors.is_empty() {
            return Err(errors);
        }

        Ok(Self {
            name,
            // Unwraps are meh.. but not critical. `errors.is_empty()` check ensures no panics here
            id: id.unwrap(),
            group_id: group_id.unwrap(),
        })
    }
}

let result = User::builder()
    .name("username")
    .id("a1da0850-03dc-4f53-8e54-e609b28d17e8")
    .group_id("fbb1efc2-bd9b-425f-a97d-b7ffc6430a1b")
    .build();

if let Err(errors) = result {
    // handle errors
}

Did you consider this possibility with bon?

zanedp commented 3 weeks ago

This would be a great example to have in the documentation. I was looking for exactly the kind of thing you're showing here where the builder can succeed or fail to build, and couldn't find it, and then luckily saw it here in this issue.

Veetaha commented 3 weeks ago

Makes sense, I'll add it to the docs together with some other common patterns with the coming release.

Veetaha commented 2 weeks ago

I added an example to the docs at the "Fallible builders" page. There are some other patterns described in the "Patterns" section. I recommend you to check them out.

See the 2.0 release blog post for details.

dzmitry-lahoda commented 6 days ago

What are possible options of validating inputs regarding ability to reference already set fields?

Veetaha commented 6 days ago

What are possible options of validating inputs regarding ability to reference already set fields?

I'm not sure I understand the full context of this question. Currently bon provides a way to generate a builder from a fallible method where all the fields are already set and available.

dzmitry-lahoda commented 6 days ago

I refer to this text

If you have a use case for some convenience attributes to do automatic validations using the #[builder] macro with the struct syntax, then add a 👍 reaction to this Github issue.

If to introduce convenience attributes to do automatic validations, there is problem if need to cross validate fields, so one field validation depends on how other field validated.