From 495c2106a2a9f52a629a3cba66d1662015f93868 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sun, 19 Nov 2023 15:11:17 +0100 Subject: [PATCH] Add example of parsing JSON with serde --- Cargo.lock | 35 ++++++---- examples/serde_complex/Cargo.toml | 13 ++++ examples/serde_complex/src/main.rs | 96 ++++++++++++++++++++++++++ nutype_macros/src/common/gen/traits.rs | 7 +- test_suite/tests/any.rs | 2 +- 5 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 examples/serde_complex/Cargo.toml create mode 100644 examples/serde_complex/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 37451b8..d078757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -187,7 +187,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -204,9 +204,9 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -315,22 +315,31 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.164" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] +[[package]] +name = "serde_complex" +version = "0.1.0" +dependencies = [ + "nutype", + "serde", + "serde_json", +] + [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.39", ] [[package]] @@ -346,9 +355,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -377,9 +386,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", diff --git a/examples/serde_complex/Cargo.toml b/examples/serde_complex/Cargo.toml new file mode 100644 index 0000000..f724a54 --- /dev/null +++ b/examples/serde_complex/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "serde_complex" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Serhii Potapov "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nutype = { path = "../../nutype", features = ["serde"] } +serde = { version = "1.0.192", features = ["derive"] } +serde_json = "1.0.108" diff --git a/examples/serde_complex/src/main.rs b/examples/serde_complex/src/main.rs new file mode 100644 index 0000000..4b5cd32 --- /dev/null +++ b/examples/serde_complex/src/main.rs @@ -0,0 +1,96 @@ +use nutype::nutype; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +struct Product { + name: Name, + image_url: ImageUrl, + price: Price, +} + +#[nutype( + sanitize(trim), + validate(not_empty, char_len_max = 50), + derive(Debug, Clone, PartialEq, AsRef, Serialize, Deserialize) +)] +struct Name(String); + +#[nutype( + sanitize(trim), + validate( + predicate = |url| url.starts_with("https://") && url.ends_with(".jpg") + ), + derive(Debug, Clone, PartialEq, AsRef, Serialize, Deserialize) +)] +struct ImageUrl(String); + +// Note: in the real world, you should use decimal instead of float to represent price. +#[nutype( + validate(greater = 0.0, less = 1000_000.0), + derive(Debug, Clone, PartialEq, AsRef, Serialize, Deserialize) +)] +struct Price(f64); + +fn main() { + { + // Invalid because name is empty + let json = r#" + { + "name": " ", + "image_url": "https://example.com/image.jpg", + "price": 9.99 + } + "#; + let res: Result = serde_json::from_str(json); + let err = res.unwrap_err(); + assert!(err.to_string().contains("empty, expected valid Name")); + } + + { + // Invalid because image_url does not end with ".jpg" + let json = r#" + { + "name": "FlySniper", + "image_url": "https://example.com/image.png", + "price": 9.99 + } + "#; + let res: Result = serde_json::from_str(json); + let err = res.unwrap_err(); + assert!(err.to_string().contains("invalid, expected valid ImageUrl")); + } + + { + // Invalid because the price is negative + let json = r#" + { + "name": "FlySniper", + "image_url": "https://example.com/image.jpg", + "price": -0.1 + } + "#; + let res: Result = serde_json::from_str(json); + let err = res.unwrap_err(); + assert!(err.to_string().contains("too small, expected valid Price")); + } + + { + // Valid product + let json = r#" + { + "name": "FlySniper\n", + "image_url": "https://example.com/image.jpg", + "price": 9.99 + } + "#; + let product: Product = serde_json::from_str(json).unwrap(); + assert_eq!( + product, + Product { + name: Name::new("FlySniper").unwrap(), + image_url: ImageUrl::new("https://example.com/image.jpg").unwrap(), + price: Price::new(9.99).unwrap(), + } + ) + } +} diff --git a/nutype_macros/src/common/gen/traits.rs b/nutype_macros/src/common/gen/traits.rs index 5916414..fab7001 100644 --- a/nutype_macros/src/common/gen/traits.rs +++ b/nutype_macros/src/common/gen/traits.rs @@ -229,8 +229,13 @@ pub fn gen_impl_trait_serde_deserialize( ) -> TokenStream { let inner_type: InnerType = inner_type.into(); let raw_value_to_result: TokenStream = if maybe_error_type_name.is_some() { + let type_name_str = type_name.to_string(); quote! { - #type_name::new(raw_value).map_err(::custom) + #type_name::new(raw_value).map_err(|validation_error| { + // Add a hint about which type is causing the error, + let err_msg = format!("{validation_error}, expected valid {}", #type_name_str); + ::custom(err_msg) + }) } } else { quote! { diff --git a/test_suite/tests/any.rs b/test_suite/tests/any.rs index 842526d..471a56b 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -284,7 +284,7 @@ mod traits { { let err = serde_json::from_str::("{\"x\":7,\"y\":9}").unwrap_err(); - assert_eq!(err.to_string(), "invalid"); + assert_eq!(err.to_string(), "invalid, expected valid LinePoint"); } {