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

Support input validation in builders #34

Open
mirosval opened this issue Aug 5, 2024 · 8 comments
Open

Support input validation in builders #34

mirosval opened this issue Aug 5, 2024 · 8 comments
Labels
design needed The feature requires more design effort feature request A new feature is requested

Comments

@mirosval
Copy link

mirosval commented Aug 5, 2024

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 Veetaha added design needed The feature requires more design effort feature request A new feature is requested labels Aug 5, 2024
@Veetaha
Copy link
Contributor

Veetaha commented Aug 5, 2024

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
Copy link

zanedp commented Aug 23, 2024

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
Copy link
Contributor

Veetaha commented Aug 23, 2024

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

@Veetaha
Copy link
Contributor

Veetaha commented Aug 26, 2024

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
Copy link
Contributor

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

@Veetaha
Copy link
Contributor

Veetaha commented Sep 8, 2024

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
Copy link
Contributor

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.

@Veetaha
Copy link
Contributor

Veetaha commented Oct 25, 2024

Hey guys, I've created an attribute #[builder(with = closure)] that allows customizing the setters.
Among other, it also accepts the following syntax: #[builder(with = |param: ty, ...| -> Result<_, ErrorType> { custom_logic })]. It can be used to make the setter fallible. Example:

#[derive(bon::Builder)]
struct Example {
    #[builder(with = |string: &str| -> Result<_, std::num::ParseIntError> {
        string.parse()
    })]
    level: u32,

    #[builder(with = |name: String| -> anyhow::Result<_> {
        if name.len() > 10 {
            return Err(anyhow::anyhow!("name is too long: {name}"));
        }
        Ok(name)
    })]
    name: String,
}

Example::builder()
    .level("10")?
    .name("hello".to_string())?
    .build();

Example::builder()
    .level("10")?
    // Setter that accepts `Option<String>` is also fallible
    .maybe_name(Some("hello".to_string()))?
    .build();

Of course you don't have to write all the validation logic inside of an attribute. You can write it in a separate function and just invoke that one inside of the closure. Unfortunatelly, this syntax requires a closure. This is needed because the builder macro needs to see the signature of the function (input types and the type of the result/error).

As a last resort it's possible to define custom methods (including custom setters) on the builder struct itself. An example of that is in #145 PR description, but I'll add extensive documentation about that soon.

It's not released yet, but this feature is already in master (merged via #145). I'll release it as part of bon 3.0. Let me know if you have any feedback.

cc all the people who put a like under the issue:

@MeGaGiGaGon, @yeswalrus, @Songtronix, @dzmitry-lahoda, @elefant-dev, @wilzbach, @dsully, @ferreira-tb and other who commented (they get a notification automatically).

@Veetaha Veetaha changed the title Input-validating builders Support input validation in builders Nov 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design needed The feature requires more design effort feature request A new feature is requested
Projects
None yet
Development

No branches or pull requests

4 participants