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

Integration with web frameworks #12

Closed
jprochazk opened this issue Mar 26, 2023 · 13 comments
Closed

Integration with web frameworks #12

jprochazk opened this issue Mar 26, 2023 · 13 comments
Labels
documentation Improvements or additions to documentation
Milestone

Comments

@jprochazk
Copy link
Owner

jprochazk commented Mar 26, 2023

It should already be possible to integrate in a straightforward way using the Unvalidated type, the only part that's missing is converting the validation errors into some output, but that should be application-specific:

#[derive(Validate)]
struct NewPost {
  #[garde(required)]
  title: String,
  #[garde(required, length(max=5000))]
  content: String,
}

struct CreatedPost {
  // ...
}

// you probably don't want to use `anyhow` for this
#[post("/post")]
async fn create_post(
  State(db): State<Database>,
  Form(post): Form<Unvalidated<NewPost>>,
) -> Result<CreatedPost, anyhow::Error>> {
  let post = post.validate(&())?;
  // persist post, which returns `CreatedPost`
  let post = db.create_post(post).await?;
  Ok(post)
}

// With https://github.com/jprochazk/garde/issues/3 implemented:
// The UI would fetch this on load and use it to attach client-side
// validation onto the form. It would also be possible to use this
// to generate the form server-side if you don't have a single-page
// app.
#[get("/contraints/post")]
async fn get_create_post_form_constraints() -> Json<Constraints> {
  Json(<NewPost as Validate>::constraints())
}
@jprochazk jprochazk added the documentation Improvements or additions to documentation label Mar 26, 2023
@jprochazk jprochazk added this to the v1.0.0 milestone Mar 26, 2023
@Altair-Bueno
Copy link
Contributor

I created an integration for axum. Still working on tests, docs and CI. But the base source should working, if you want to try it out :)

https://github.com/Altair-Bueno/axum-garde

@jprochazk
Copy link
Owner Author

Looks good! I have a few questions:

Would you be willing to merge it into this repo, so that it can be kept up-to-date more easily?

Why is the 2nd type parameter necessary here? Do you think adding an associated type to Unwrapable (which would contain the inner type) would allow you to remove it?

Do you think it would make sense to create a Config type which would be stored in the app state, and could contain any user-defined Context types? Those could then be extracted inside the FromRequestParts/FromRequest parts and passed into validate.

@Altair-Bueno
Copy link
Contributor

Would you be willing to merge it into this repo, so that it can be kept up-to-date more easily?

Yes, that would be easier. Let me finish it first

Why is the 2nd type parameter necessary here? Do you think adding an associated type to Unwrapable (which would contain the inner type) would allow you to remove it?

Becase we target the final deserialized value (eg Foo), but we require the type information provided by the extractor (eg Json). It's the same API that WithRejection has.

I will sleep over the idea tho, maybe there is some way to flip the trait so we can have both the type information and the final target.

Do you think it would make sense to create a Config type which would be stored in the app state, and could contain any user-defined Context types? Those could then be extracted inside the FromRequestParts/FromRequest parts and passed into validate.

It can be done, i just didn't bother (yet). Adding it rn

@Altair-Bueno
Copy link
Contributor

I will sleep over the idea tho, maybe there is some way to flip the trait so we can have both the type information and the final target.

I found another API design that is both simpler and easier to use, but of course there is a catch. Instead of creating a new trait for unwrapping tuple structs, we leverage the Deref trait to obtain a reference to some type T. The catch is that we cannot obtain an instance of Valid, as we are deserializing into T and not Valid<T>

One alternative would be to impl Serialize/Deserialize on garde and perform validation in situ, but that wouldn't allow usage of Validate::Context and i don't know if that's even possible

Source for this idea is available at the ref/ditch_valid_typestate branch: https://github.com/Altair-Bueno/axum-garde/tree/ref/ditch_valid_typestate

@jprochazk
Copy link
Owner Author

jprochazk commented Apr 1, 2023

I think Valid could have an unsafe unchecked constructor which could be used in extractors like this. The invariant would be that Valid::<T>::new_unchecked(v) is safe if T::validate(&v, &ctx).is_ok() == true. It seems to me like it's well in line with other "parsing" or "validation" APIs in Rust (e.g. constructing str from [u8]).
What do you think?

@Altair-Bueno
Copy link
Contributor

Altair-Bueno commented Apr 1, 2023

That would allow end users to construct Valid<T>, but we wouldn't be able to do it from within the axum extractor. We still need more information: the target value T, the extractor (eg Json<T>) and a trait to unwrap T in order to construct Valid<T>. Otherwise, we could only construct Valid<Json<T>>, which couples axum extractors with the business logic

Note: I just realised that Valid<Json<T>> wouldn't work as into_inner requires that Json<T>: Validate

Note2: An alternative solution to the whole PhantomData & trait mess

  • Make WithValidation constructor private
  • impl <T> From<WithValidation<Json<T>>> for Valid<T> and vice versa. Repeat for Path, Form, ...
  • Users can now painlessly transform between them. Still no direct Valid<T> extraction, but calling .into() should be acceptable
  • External extractors wouldn't benefit from From impls, but uses would still be able to unwrap the values and manually construct Valid<T>

Note3: After playing with the proposed solution on note2, it doesn't feel right. Making the constructor private makes the usage of WithValidation not enjoyable. Making "Valid" variants of extractors (ValidJson, ValidForm, etc) would imply compatibility issues with other axum libraries, a lot of repeated code and the same bad developer UX as note2.

@Altair-Bueno
Copy link
Contributor

Altair-Bueno commented Apr 2, 2023

Could you please take a look at the axum-garde? Implementation should be ready. It includes a couple of examples, docs, tests and a lot of integrations with multiple axum extractors

@jprochazk

@jprochazk
Copy link
Owner Author

LGTM! I think the default features should include json, form, and query to be closer to axum's default features. That should make it easier to just cargo add garde axum-garde and start using it.

@jprochazk
Copy link
Owner Author

Keeping this open for an integration with actix-web.

@LucasPickering
Copy link

So I've never used this library, but I have used validator. I had an idea around integrating validation into serde and did some googling to see if anyone has done it, and ended up here. I'm wondering if this might transparently solve your web framework integration. Essentially, garde could provide its own derive macro for Deserialize. The generated Deserialize implementation would do normal deserialization (which would be delegated to an identical struct), then apply validation on top.

use garde::{Deserialize, Validate, Valid};

#[derive(Deserialize, Validate)]
struct User<'a> {
    #[garde(ascii, length(min=3, max=25))]
    username: &'a str,
    #[garde(length(min=15))]
    password: &'a str,
}

// This would fail because of validation errors
let user = serde_json::from_str("{\"username\": \"no\", \"password\": \"bad\"}").unwrap();

The garde version of the Deserialize derive would generate something like:

use serde::Deserialize;

#[derive(Deserialize, Validate)]
struct _User<'a> {
    #[garde(ascii, length(min=3, max=25))]
    username: &'a str,
    #[garde(length(min=15))]
    password: &'a str,
}

impl<'de> Deserialize<'de> for User {
    fn deserialize<D>(deserializer: D) -> Result<User, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = _User::deserialize(deserializer);
        value.validate()  // We'd need to figure out how to map this error
    }
}

The two main issues I see here are:

  • How to generate the inner _User struct. We'd need to make sure to maintain support for all of serde's attributes, so those have to get forwarded to the inner Deserialize derive. I haven't worked with derive macros before so I'm not sure if that's possible.
  • How do we convert validation errors into serde errors in a way that's useful?

The major downside here of course is there's no simple way to deserialize your type without validating. Personally I've never wanted to do that but someone might at some point. It also means you end up duplicating every struct in the generated code, which may or may not slow down compilation.

I'm happy to work on answering these questions and implementing this, but I wanted to check that my idea makes sense and is something you'd want in garde before doing so. Let me know if I'm crazy or something.

@jprochazk
Copy link
Owner Author

jprochazk commented Aug 10, 2023

Unlike in validator, garde requires a context passed to validate calls. This context is acquired from the framework equivalent of "app state", which will always require framework-specific code. I don't think it's possible to pass the context into deserialize.

There's the Unvalidated type, which I was hoping would act as a kind of bridge if there is no integration available for your favorite framework. You'd do the equivalent of extracting Json<Unvalidated<T>>, then call validate on it in the body of your handler.

@LucasPickering
Copy link

Of course! That makes sense

@jprochazk
Copy link
Owner Author

We've now got two "proper" integrations:

It should be straightforward to use Unvalidated in any other framework that supports some kind of "extractor" via Deserialize.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants