Skip to content

Commit

Permalink
Support #[serde(validate = "some_function")] for container, compati…
Browse files Browse the repository at this point in the history
…ble with validator crate
  • Loading branch information
zao111222333 committed Feb 11, 2025
1 parent aaccac7 commit 780b391
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 2 deletions.
17 changes: 15 additions & 2 deletions serde_derive/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub fn expand_derive_deserialize(input: &mut syn::DeriveInput) -> syn::Result<To
let params = Parameters::new(&cont);
let (de_impl_generics, _, ty_generics, where_clause) = split_with_de_lifetime(&params);
let body = Stmts(deserialize_body(&cont, &params));
let vaildated_body = validate_body(&cont, body);
let delife = params.borrowed.de_lifetime();
let serde = cont.attrs.serde_path();

Expand All @@ -40,7 +41,7 @@ pub fn expand_derive_deserialize(input: &mut syn::DeriveInput) -> syn::Result<To
__D: #serde::Deserializer<#delife>,
{
#used
#body
#vaildated_body
}
}
}
Expand All @@ -54,7 +55,7 @@ pub fn expand_derive_deserialize(input: &mut syn::DeriveInput) -> syn::Result<To
where
__D: #serde::Deserializer<#delife>,
{
#body
#vaildated_body
}

#fn_deserialize_in_place
Expand Down Expand Up @@ -273,6 +274,18 @@ fn borrowed_lifetimes(cont: &Container) -> BorrowedLifetimes {
}
}

fn validate_body(cont: &Container, body: Stmts) -> TokenStream {
if let Some(vaildate) = cont.attrs.vaildate() {
quote! {
let __body = { #body }?;
#vaildate(&__body).map_err(_serde::de::Error::custom)?;
Ok(__body)
}
} else {
quote! { #body }
}
}

fn deserialize_body(cont: &Container, params: &Parameters) -> Fragment {
if cont.attrs.transparent() {
deserialize_transparent(cont, params)
Expand Down
12 changes: 12 additions & 0 deletions serde_derive/src/internals/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub struct Container {
remote: Option<syn::Path>,
identifier: Identifier,
serde_path: Option<syn::Path>,
vaildate: Option<syn::ExprPath>,
is_packed: bool,
/// Error message generated when type can't be deserialized
expecting: Option<String>,
Expand Down Expand Up @@ -256,6 +257,7 @@ impl Container {
let mut remote = Attr::none(cx, REMOTE);
let mut field_identifier = BoolAttr::none(cx, FIELD_IDENTIFIER);
let mut variant_identifier = BoolAttr::none(cx, VARIANT_IDENTIFIER);
let mut vaildate = Attr::none(cx, VALIDATE);
let mut serde_path = Attr::none(cx, CRATE);
let mut expecting = Attr::none(cx, EXPECTING);
let mut non_exhaustive = false;
Expand Down Expand Up @@ -486,6 +488,11 @@ impl Container {
if let Some(path) = parse_lit_into_path(cx, CRATE, &meta)? {
serde_path.set(&meta.path, path);
}
} else if meta.path == VALIDATE {
// #[serde(validate = "...")]
if let Some(path) = parse_lit_into_expr_path(cx, VALIDATE, &meta)? {
vaildate.set(&meta.path, path);
}
} else if meta.path == EXPECTING {
// #[serde(expecting = "a message")]
if let Some(s) = get_lit_str(cx, EXPECTING, &meta)? {
Expand Down Expand Up @@ -539,6 +546,7 @@ impl Container {
remote: remote.get(),
identifier: decide_identifier(cx, item, field_identifier, variant_identifier),
serde_path: serde_path.get(),
vaildate: vaildate.get(),
is_packed,
expecting: expecting.get(),
non_exhaustive,
Expand Down Expand Up @@ -597,6 +605,10 @@ impl Container {
self.remote.as_ref()
}

pub fn vaildate(&self) -> Option<&syn::ExprPath> {
self.vaildate.as_ref()
}

pub fn is_packed(&self) -> bool {
self.is_packed
}
Expand Down
1 change: 1 addition & 0 deletions serde_derive/src/internals/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub const TRANSPARENT: Symbol = Symbol("transparent");
pub const TRY_FROM: Symbol = Symbol("try_from");
pub const UNTAGGED: Symbol = Symbol("untagged");
pub const VARIANT_IDENTIFIER: Symbol = Symbol("variant_identifier");
pub const VALIDATE: Symbol = Symbol("validate");
pub const WITH: Symbol = Symbol("with");

impl PartialEq<Symbol> for Ident {
Expand Down
1 change: 1 addition & 0 deletions test_suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ serde = { path = "../serde", features = ["rc"] }
serde_derive = { path = "../serde_derive", features = ["deserialize_in_place"] }
serde_test = "1.0.176"
trybuild = { version = "1.0.97", features = ["diff"] }
validator = { version = "0.20", features = ["derive"] }
209 changes: 209 additions & 0 deletions test_suite/tests/test_validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
use serde_derive::{Deserialize, Serialize};
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
use std::fmt::Display;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(validate = "validate_struct")]
struct Struct {
a: u16,
}

fn validate_struct(deserialized: &Struct) -> Result<(), impl Display> {
if deserialized.a == 0 {
return Err("field `a` can not be zero");
}
Ok(())
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(validate = "validate_tuple_struct")]
struct TupleStruct(u16);

fn validate_tuple_struct(deserialized: &TupleStruct) -> Result<(), impl Display> {
if deserialized.0 == 0 {
return Err("field `0` can not be zero");
}
Ok(())
}

#[test]
fn test_struct() {
assert_de_tokens(
&Struct { a: 1 },
&[
Token::Struct {
name: "Struct",
len: 1,
},
Token::Str("a"),
Token::U16(1),
Token::StructEnd,
],
);

assert_de_tokens_error::<Struct>(
&[
Token::Struct {
name: "Struct",
len: 1,
},
Token::Str("a"),
Token::U16(0),
Token::StructEnd,
],
"field `a` can not be zero",
);
}

#[test]
fn test_tuple_struct() {
assert_de_tokens(
&TupleStruct(1),
&[
Token::TupleStruct {
name: "TupleStruct",
len: 1,
},
Token::U16(1),
Token::TupleStructEnd,
],
);

assert_de_tokens_error::<TupleStruct>(
&[
Token::TupleStruct {
name: "TupleStruct",
len: 1,
},
Token::U16(0),
Token::TupleStructEnd,
],
"field `0` can not be zero",
);
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(validate = "validate_struct_variant")]
enum StructVariant {
Struct { a: u16 },
}

fn validate_struct_variant(deserialized: &StructVariant) -> Result<(), impl Display> {
if let StructVariant::Struct { a: 0 } = deserialized {
return Err("variant `Struct.a` can not be zero");
}
Ok(())
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(validate = "validate_tuple_variant")]
enum TupleVariant {
A(u16, u16),
}

fn validate_tuple_variant(deserialized: &TupleVariant) -> Result<(), impl Display> {
if let TupleVariant::A(0, _) = deserialized {
return Err("variant `A.0` can not be zero");
}
Ok(())
}

#[test]
fn test_struct_variant() {
assert_de_tokens(
&StructVariant::Struct { a: 1 },
&[
Token::StructVariant {
name: "StructVariant",
variant: "Struct",
len: 1,
},
Token::Str("a"),
Token::U16(1),
Token::StructVariantEnd,
],
);

assert_de_tokens_error::<StructVariant>(
&[
Token::StructVariant {
name: "StructVariant",
variant: "Struct",
len: 1,
},
Token::Str("a"),
Token::U16(0),
Token::StructVariantEnd,
],
"variant `Struct.a` can not be zero",
);
}

#[test]
fn test_tuple_variant() {
assert_de_tokens(
&TupleVariant::A(1, 1),
&[
Token::TupleVariant {
name: "TupleVariant",
variant: "A",
len: 2,
},
Token::U16(1),
Token::U16(1),
Token::TupleVariantEnd,
],
);

assert_de_tokens_error::<TupleVariant>(
&[
Token::TupleVariant {
name: "TupleVariant",
variant: "A",
len: 2,
},
Token::U16(0),
Token::U16(1),
Token::TupleVariantEnd,
],
"variant `A.0` can not be zero",
);
}

#[derive(Debug, PartialEq, validator::Validate, Deserialize)]
#[serde(validate = "validator::Validate::validate")]
struct ValidatorDemo {
#[validate(email)]
mail: String,
}

#[test]
fn test_validator_demo() {
assert_de_tokens(
&ValidatorDemo {
mail: "[email protected]".into(),
},
&[
Token::Struct {
name: "ValidatorDemo",
len: 1,
},
Token::Str("mail"),
Token::Str("[email protected]"),
Token::StructEnd,
],
);

assert_de_tokens_error::<ValidatorDemo>(
&[
Token::Struct {
name: "ValidatorDemo",
len: 1,
},
Token::Str("mail"),
Token::Str("email.example.com"),
Token::StructEnd,
],
"mail: Validation error: email [{\"value\": String(\"email.example.com\")}]",
);
}

0 comments on commit 780b391

Please sign in to comment.