├── .github └── workflows │ ├── CI.yml │ └── auto_approve.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── Taskfile.yml ├── examples ├── Cargo.toml ├── examples │ ├── readme.rs │ ├── validator.rs │ └── various_users.rs └── reexport │ ├── Cargo.toml │ ├── reexporter │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ └── src │ └── main.rs ├── serdev ├── Cargo.toml ├── benches │ └── transfer.rs └── src │ └── lib.rs └── serdev_derive ├── Cargo.toml └── src ├── internal.rs ├── internal ├── reexport.rs ├── target.rs └── validate.rs └── lib.rs /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | CI: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | toolchain: ['stable', 'nightly'] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: ${{ matrix.toolchain }} 22 | profile: minimal 23 | override: true 24 | 25 | - name: Run tasks 26 | run: | 27 | sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin 28 | ${{ matrix.toolchain == 'nightly' && 'task CI' || 'task CI:nobench' }} 29 | -------------------------------------------------------------------------------- /.github/workflows/auto_approve.yml: -------------------------------------------------------------------------------- 1 | # This will be removed when serdev has more than one maintainers 2 | 3 | name: auto_approve 4 | on: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - ready_for_review 11 | 12 | jobs: 13 | auto_approve: 14 | if: | 15 | github.event.pull_request.user.login == 'kanarus' && 16 | !github.event.pull_request.draft 17 | permissions: 18 | pull-requests: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: hmarr/auto-approve-action@v4 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["serdev", "serdev_derive"] 4 | exclude = ["examples"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 kanarus 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

SerdeV

3 | SerdeV - Serde with Validation 4 |
5 | 6 |
7 | 8 | - Just a wrapper of Serde and 100% compatible 9 | - Declarative validation in deserialization by `#[serde(validate = "...")]` 10 | 11 |
12 | 13 | License 14 | 15 | 16 | CI status 17 | 18 | 19 | crates.io 20 | 21 |
22 | 23 | 24 | ## Example 25 | 26 | ```toml 27 | [dependencies] 28 | serdev = "0.2" 29 | serde_json = "1.0" 30 | ``` 31 | 32 | ```rust 33 | use serdev::{Serialize, Deserialize}; 34 | 35 | #[derive(Serialize, Deserialize, Debug)] 36 | #[serde(validate = "Self::validate")] 37 | struct Point { 38 | x: i32, 39 | y: i32, 40 | } 41 | 42 | impl Point { 43 | fn validate(&self) -> Result<(), impl std::fmt::Display> { 44 | if self.x * self.y > 100 { 45 | return Err("x * y must not exceed 100") 46 | } 47 | Ok(()) 48 | } 49 | } 50 | 51 | fn main() { 52 | let point = serde_json::from_str::(r#" 53 | { "x" : 1, "y" : 2 } 54 | "#).unwrap(); 55 | 56 | // Prints point = Point { x: 1, y: 2 } 57 | println!("point = {point:?}"); 58 | 59 | let error = serde_json::from_str::(r#" 60 | { "x" : 10, "y" : 20 } 61 | "#).unwrap_err(); 62 | 63 | // Prints error = x * y must not exceed 100 64 | println!("error = {error}"); 65 | } 66 | ``` 67 | 68 | Of course, you can use it in combination with some validation tools like validator! ( full example ) 69 | 70 | 71 | ## Attribute 72 | 73 | - `#[serde(validate = "function")]` 74 | 75 | Automatically validate by the `function` in deserialization. The `function` must be callable as `fn(&self) -> Result<(), impl Display>`.\ 76 | Errors are converted to a `String` internally and passed to `serde::de::Error::custom`. 77 | 78 | - `#[serde(validate(by = "function", error = "Type"))]` 79 | 80 | Using given `Type` for validation error without internal conversion. The `function` must explicitly return `Result<(), Type>`.\ 81 | This may be preferred when you need better performance _even in error cases_.\ 82 | For **no-std** use, this is the only way supported. 83 | 84 | Both `"function"` and `"Type"` accept path like `"crate::util::validate"`. 85 | 86 | Additionally, `#[serdev(crate = "path::to::serdev")]` is supported for reexport from another crate. 87 | 88 | 89 | ## License 90 | 91 | Licensed under MIT LICENSE ( [LICENSE](https://github.com/ohkami-rs/serdev/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) ). 92 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | CI: 5 | deps: 6 | - test 7 | - check 8 | - bench-dryrun 9 | 10 | CI:nobench: 11 | deps: 12 | - test 13 | - check 14 | 15 | test: 16 | deps: 17 | - test:doc 18 | - test:lib 19 | - test:examples 20 | 21 | check: 22 | deps: 23 | - check:lib 24 | - check:examples 25 | 26 | bench: 27 | deps: 28 | - bench:all 29 | 30 | bench-dryrun: 31 | deps: 32 | - bench:dryrun 33 | 34 | ##### test ##### 35 | 36 | test:doc: 37 | cmds: 38 | - cargo test --doc --features DEBUG 39 | 40 | test:lib: 41 | cmds: 42 | - cargo test --lib --features DEBUG 43 | 44 | test:examples: 45 | dir: examples 46 | cmds: 47 | - cargo run --example readme 48 | - cargo run --example validator 49 | - cargo run --example various_users 50 | - cd reexport && cargo run 51 | 52 | ##### check ##### 53 | 54 | check:lib: 55 | cmds: 56 | - cargo check 57 | 58 | check:examples: 59 | dir: examples 60 | cmds: 61 | - cargo check --examples 62 | 63 | ##### bench ##### 64 | 65 | bench:all: 66 | cmds: 67 | - cargo bench 68 | 69 | bench:dryrun: 70 | cmds: 71 | - cargo bench --no-run 72 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["."] 4 | exclude = ["reexport"] 5 | 6 | [package] 7 | name = "examples" 8 | version = "0.0.0" 9 | edition = "2021" 10 | 11 | [dev-dependencies] 12 | serdev = { path = "../serdev" } 13 | serde_json = { version = "1.0" } 14 | validator = { version = "0.16", features = ["derive"] } -------------------------------------------------------------------------------- /examples/examples/readme.rs: -------------------------------------------------------------------------------- 1 | use serdev::{Serialize, Deserialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | #[serde(validate = "Self::validate")] 5 | struct Point { 6 | x: i32, 7 | y: i32, 8 | } 9 | 10 | impl Point { 11 | fn validate(&self) -> Result<(), impl std::fmt::Display> { 12 | if self.x * self.y > 100 { 13 | return Err("x * y must not exceed 100") 14 | } 15 | Ok(()) 16 | } 17 | } 18 | 19 | fn main() { 20 | let point = serde_json::from_str::(r#" 21 | { "x" : 1, "y" : 2 } 22 | "#).unwrap(); 23 | 24 | // Prints point = Point { x: 1, y: 2 } 25 | println!("point = {point:?}"); 26 | 27 | let error = serde_json::from_str::(r#" 28 | { "x" : 10, "y" : 20 } 29 | "#).unwrap_err(); 30 | 31 | // Prints error = x * y must not exceed 100 32 | println!("error = {error}"); 33 | } 34 | -------------------------------------------------------------------------------- /examples/examples/validator.rs: -------------------------------------------------------------------------------- 1 | use serdev::Deserialize; 2 | use validator::{Validate, ValidationError}; 3 | 4 | #[derive(Deserialize, Debug, PartialEq, Validate)] 5 | #[serde(validate = "Validate::validate")] 6 | struct SignupData { 7 | #[validate(email)] 8 | mail: String, 9 | #[validate(url)] 10 | site: String, 11 | #[validate(length(min = 1), custom(function = "validate_unique_username"))] 12 | #[serde(rename = "firstName")] 13 | first_name: String, 14 | #[validate(range(min = 18, max = 20))] 15 | age: u32, 16 | #[validate(range(min = 0.0, max = 100.0))] 17 | height: f32, 18 | } 19 | 20 | fn validate_unique_username(username: &str) -> Result<(), ValidationError> { 21 | if username == "xXxShad0wxXx" { 22 | // the value of the username will automatically be added later 23 | return Err(ValidationError::new("terrible_username")); 24 | } 25 | 26 | Ok(()) 27 | } 28 | 29 | fn main() { 30 | let signupdata = serde_json::from_str::(r#" 31 | { 32 | "mail": "serdev@ohkami.rs", 33 | "site": "https://ohkami.rs", 34 | "firstName": "serdev", 35 | "age": 20, 36 | "height": 0.0 37 | } 38 | "#).unwrap(); 39 | assert_eq!(signupdata, SignupData { 40 | mail: String::from("serdev@ohkami.rs"), 41 | site: String::from("https://ohkami.rs"), 42 | first_name: String::from("serdev"), 43 | age: 20, 44 | height: 0.0 45 | }); 46 | 47 | let error = serde_json::from_str::(r#" 48 | { 49 | "mail": "serdev@ohkami.rs", 50 | "site": "https://ohkami.rs", 51 | "firstName": "serdev", 52 | "age": 0, 53 | "height": 0.0 54 | } 55 | "#).unwrap_err(); 56 | println!("error: {error}"); 57 | } 58 | -------------------------------------------------------------------------------- /examples/examples/various_users.rs: -------------------------------------------------------------------------------- 1 | use serdev::{Serialize, Deserialize}; 2 | 3 | 4 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 5 | struct User { 6 | name: String, 7 | age: usize, 8 | } 9 | 10 | #[derive(Debug, PartialEq, Deserialize)] 11 | #[serde(validate = "Self::validate")] 12 | struct VUser { 13 | name: String, 14 | age: usize, 15 | } 16 | impl VUser { 17 | fn validate(&self) -> Result<(), impl std::fmt::Display> { 18 | if self.name.is_empty() { 19 | return Err("`name` must not be empty") 20 | } 21 | Ok(()) 22 | } 23 | } 24 | 25 | #[derive(Debug, PartialEq, Deserialize)] 26 | #[serde(validate(by = "Self::validate", error = "&'static str"))] 27 | struct EUser { 28 | name: String, 29 | age: usize, 30 | } 31 | impl EUser { 32 | fn validate(&self) -> Result<(), &'static str> { 33 | if self.name.is_empty() { 34 | return Err("`name` must not be empty") 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | #[derive(Debug, PartialEq, Deserialize)] 41 | #[serde(validate = "Self::validate")] 42 | struct GUser<'n, Name: From+ToString, Age: From> { 43 | name: Name, 44 | age: Age, 45 | nickname: Option<&'n str> 46 | } 47 | impl<'n, Name: From+ToString, Age: From> GUser<'n, Name, Age> { 48 | fn validate(&self) -> Result<(), impl std::fmt::Display> { 49 | if self.name.to_string().is_empty() { 50 | return Err("`name` must not be empty") 51 | } 52 | Ok(()) 53 | } 54 | } 55 | 56 | fn main() { 57 | assert_eq!( 58 | serde_json::to_string(&User { 59 | name: String::from("serdev"), 60 | age: 0 61 | }).unwrap(), 62 | r#"{"name":"serdev","age":0}"# 63 | ); 64 | assert_eq!( 65 | serde_json::from_str::( 66 | r#"{"age":4,"name":"ohkami"}"# 67 | ).unwrap(), 68 | User { 69 | name: String::from("ohkami"), 70 | age: 4 71 | } 72 | ); 73 | 74 | assert_eq!( 75 | serde_json::from_str::( 76 | r#"{"age":4,"name":"ohkami"}"# 77 | ).unwrap(), 78 | VUser { 79 | name: String::from("ohkami"), 80 | age: 4 81 | } 82 | ); 83 | assert_eq!( 84 | serde_json::from_str::( 85 | r#"{"age":4,"name":""}"# 86 | ).unwrap_err().to_string(), 87 | "`name` must not be empty" 88 | ); 89 | 90 | assert_eq!( 91 | serde_json::from_str::( 92 | r#"{"age":4,"name":"ohkami"}"# 93 | ).unwrap(), 94 | EUser { 95 | name: String::from("ohkami"), 96 | age: 4 97 | } 98 | ); 99 | assert_eq!( 100 | serde_json::from_str::( 101 | r#"{"age":4,"name":""}"# 102 | ).unwrap_err().to_string(), 103 | "`name` must not be empty" 104 | ); 105 | 106 | assert_eq!( 107 | serde_json::from_str::>( 108 | r#"{"age":4,"name":"ohkami"}"# 109 | ).unwrap(), 110 | GUser { 111 | name: String::from("ohkami"), 112 | age: 4, 113 | nickname: None 114 | } 115 | ); 116 | assert_eq!( 117 | serde_json::from_str::>( 118 | r#"{"age":4,"nickname":"wolf","name":"ohkami"}"# 119 | ).unwrap(), 120 | GUser { 121 | name: String::from("ohkami"), 122 | age: 4, 123 | nickname: Some("wolf") 124 | } 125 | ); 126 | assert_eq!( 127 | serde_json::from_str::>( 128 | r#"{"age":4,"nickname":"wolf","name":""}"# 129 | ).unwrap_err().to_string(), 130 | "`name` must not be empty" 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /examples/reexport/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["reexporter", "."] 4 | 5 | [package] 6 | name = "user" 7 | version = "0.0.0" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | reexporter = { path = "./reexporter" } 12 | serde_json = { version = "1.0" } -------------------------------------------------------------------------------- /examples/reexport/reexporter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reexporter" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serdev = { path = "../../../serdev" } -------------------------------------------------------------------------------- /examples/reexport/reexporter/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod private { 2 | pub use ::serdev; 3 | } 4 | -------------------------------------------------------------------------------- /examples/reexport/src/main.rs: -------------------------------------------------------------------------------- 1 | use reexporter::private::serdev::{Serialize, Deserialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | #[serdev(crate = "reexporter::private::serdev")] 5 | #[serde(validate = "Self::validate")] 6 | struct Point { 7 | x: i32, 8 | y: i32, 9 | } 10 | 11 | impl Point { 12 | fn validate(&self) -> Result<(), impl std::fmt::Display> { 13 | if self.x * self.y > 100 { 14 | return Err("x * y must not exceed 100") 15 | } 16 | Ok(()) 17 | } 18 | } 19 | 20 | fn main() { 21 | let point = serde_json::from_str::(r#" 22 | { "x" : 1, "y" : 2 } 23 | "#).unwrap(); 24 | 25 | // Prints point = Point { x: 1, y: 2 } 26 | println!("point = {point:?}"); 27 | 28 | let error = serde_json::from_str::(r#" 29 | { "x" : 10, "y" : 20 } 30 | "#).unwrap_err(); 31 | 32 | // Prints error = x * y must not exceed 100 33 | println!("error = {error}"); 34 | } 35 | -------------------------------------------------------------------------------- /serdev/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serdev" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["kanarus "] 6 | documentation = "https://docs.rs/serdev" 7 | homepage = "https://crates.io/crates/serdev" 8 | repository = "https://github.com/ohkami-rs/serdev" 9 | readme = "../README.md" 10 | license = "MIT" 11 | description = "SerdeV - Serde with Validation" 12 | keywords = ["serde", "validation", "serialization"] 13 | categories = ["encoding", "rust-patterns", "no-std", "no-std::no-alloc"] 14 | 15 | [dependencies] 16 | serdev_derive = { version = "=0.2.0", path = "../serdev_derive" } 17 | serde = { version = "1", features = ["derive"] } 18 | 19 | [dev-dependencies] 20 | serde_json = "1.0" # for README doc test 21 | rand = "0.8" # for bench 22 | 23 | [features] 24 | nightly = [] 25 | DEBUG = [] 26 | 27 | ### DEBUG ### 28 | #default = ["DEBUG"] -------------------------------------------------------------------------------- /serdev/benches/transfer.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use std::sync::LazyLock; 6 | use test::bench::Bencher; 7 | 8 | #[allow(unused)] 9 | struct S { 10 | a: String, 11 | b: usize, 12 | c: Vec, 13 | } 14 | 15 | #[derive(Clone)] 16 | struct SP { 17 | a: String, 18 | b: usize, 19 | c: Vec, 20 | } 21 | 22 | #[allow(unused)] 23 | #[derive(Clone)] 24 | struct T { 25 | d: usize, 26 | e: String, 27 | } 28 | 29 | static CASES: LazyLock<[SP; 100]> = LazyLock::new(|| { 30 | use rand::{thread_rng, Rng}; 31 | 32 | fn random_string() -> String { 33 | use rand::distributions::{DistString, Alphanumeric}; 34 | let len = thread_rng().gen_range(0..100); 35 | Alphanumeric.sample_string(&mut thread_rng(), len) 36 | } 37 | 38 | fn random_uint() -> usize { 39 | thread_rng().gen::() 40 | } 41 | 42 | (0..100).map(|_| SP { 43 | a: random_string(), 44 | b: random_uint(), 45 | c: (0..100).map(|_| T { 46 | d: random_uint(), 47 | e: random_string() 48 | }).collect() 49 | }).collect::>().try_into().ok().unwrap() 50 | }); 51 | 52 | #[bench] 53 | fn transfer_by_hand(b: &mut Bencher) { 54 | test::black_box(&*CASES); 55 | b.iter(|| -> [S; 100] { 56 | CASES.clone().map(|sp| test::black_box( 57 | S { a: sp.a, b: sp.b, c: sp.c } 58 | )) 59 | }) 60 | } 61 | 62 | #[bench] 63 | fn transfer_by_mem_transmute(b: &mut Bencher) { 64 | test::black_box(&*CASES); 65 | b.iter(|| -> [S; 100] { 66 | CASES.clone().map(|sp| test::black_box( 67 | unsafe {std::mem::transmute(sp)} 68 | )) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /serdev/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature="DEBUG", doc = include_str!("../../README.md"))] 2 | 3 | pub use serdev_derive::{Serialize, Deserialize}; 4 | pub use ::serde::ser::{self, Serialize, Serializer}; 5 | pub use ::serde::de::{self, Deserialize, Deserializer}; 6 | 7 | #[doc(hidden)] 8 | pub mod __private__ { 9 | pub use serdev_derive::consume; 10 | pub use ::serde; 11 | pub type DefaultError = ::std::string::String; 12 | pub fn default_error(e: impl std::fmt::Display) -> DefaultError {e.to_string()} 13 | } 14 | -------------------------------------------------------------------------------- /serdev_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serdev_derive" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["kanarus "] 6 | documentation = "https://docs.rs/serdev_derive" 7 | homepage = "https://crates.io/crates/serdev_derive" 8 | repository = "https://github.com/ohkami-rs/serdev_derive" 9 | readme = "../README.md" 10 | license = "MIT" 11 | description = "SerdeV - Serde with Validation" 12 | keywords = ["serde", "validation", "serialization"] 13 | categories = ["encoding", "rust-patterns", "no-std", "no-std::no-alloc"] 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | proc-macro2 = { version = "1.0" } 20 | quote = { version = "1.0" } 21 | syn = { version = "2.0", features = ["full"] } -------------------------------------------------------------------------------- /serdev_derive/src/internal.rs: -------------------------------------------------------------------------------- 1 | mod target; 2 | mod validate; 3 | mod reexport; 4 | 5 | use self::target::Target; 6 | use self::validate::Validate; 7 | use self::reexport::Reexport; 8 | 9 | use proc_macro2::{Span, TokenStream}; 10 | use quote::{format_ident, quote, ToTokens}; 11 | use syn::{Error, LitStr}; 12 | 13 | 14 | pub(super) fn Serialize(input: TokenStream) -> Result { 15 | let mut target = syn::parse2::(input.clone())?; 16 | 17 | let _ = Validate::take(target.attrs_mut())?; 18 | 19 | let (serdev, serde) = match Reexport::take(target.attrs_mut())? { 20 | None => ( 21 | quote! {::serdev}, 22 | litstr("::serdev::__private__::serde") 23 | ), 24 | Some(r) => ( 25 | r.path()?.into_token_stream(), 26 | litstr(&format!("{}::__private__::serde", r.path_str())) 27 | ) 28 | }; 29 | 30 | Ok(quote! { 31 | #[derive(#serdev::__private__::serde::Serialize)] 32 | #[serde(crate = #serde)] 33 | #[#serdev::__private__::consume] 34 | #target 35 | }) 36 | } 37 | 38 | pub(super) fn Deserialize(input: TokenStream) -> Result { 39 | let mut target = syn::parse2::(input.clone())?; 40 | 41 | let generics = target.generics().clone(); 42 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 43 | 44 | let (serdev, serde) = match Reexport::take(target.attrs_mut())? { 45 | None => ( 46 | quote! {::serdev}, 47 | litstr("::serdev::__private__::serde") 48 | ), 49 | Some(r) => ( 50 | r.path()?.into_token_stream(), 51 | litstr(&format!("{}::__private__::serde", r.path_str())) 52 | ) 53 | }; 54 | 55 | Ok(match Validate::take(target.attrs_mut())? { 56 | Some(validate) => { 57 | let proxy = target.create_proxy(format_ident!("serdev_proxy_{}", target.ident())); 58 | 59 | let target_ident = target.ident(); 60 | let proxy_ident = proxy.ident(); 61 | 62 | let transmute_from_proxy = proxy.transmute_expr("proxy", target_ident); 63 | 64 | let proxy_type_lit = litstr("e!(#proxy_ident #ty_generics).to_string()); 65 | 66 | let validate_fn = validate.function()?; 67 | let (error_ty, e_as_error_ty) = match validate.error()? { 68 | Some(ty) => ( 69 | quote! {#ty}, 70 | quote! {e} 71 | ), 72 | None => ( 73 | quote! {#serdev::__private__::DefaultError}, 74 | quote! {#serdev::__private__::default_error(e)} 75 | ) 76 | }; 77 | 78 | quote! { 79 | const _: () = { 80 | #[derive(#serdev::__private__::serde::Deserialize)] 81 | #[serde(crate = #serde)] 82 | #[allow(non_camel_case_types)] 83 | #proxy 84 | 85 | impl #impl_generics ::core::convert::TryFrom<#proxy_ident #ty_generics> for #target_ident #ty_generics 86 | #where_clause 87 | { 88 | type Error = #error_ty; 89 | 90 | #[inline] 91 | fn try_from(proxy: #proxy_ident #ty_generics) -> ::core::result::Result { 92 | let this = #transmute_from_proxy; 93 | let _: () = #validate_fn(&this).map_err(|e| #e_as_error_ty)?; 94 | Ok(this) 95 | } 96 | } 97 | 98 | #[derive(#serdev::__private__::serde::Deserialize)] 99 | #[serde(crate = #serde)] 100 | #[serde(try_from = #proxy_type_lit)] 101 | #[#serdev::__private__::consume] 102 | #target 103 | }; 104 | } 105 | } 106 | 107 | None => { 108 | quote! { 109 | #[derive(#serdev::__private__::serde::Deserialize)] 110 | #[serde(crate = #serde)] 111 | #[#serdev::__private__::consume] 112 | #target 113 | } 114 | } 115 | }) 116 | } 117 | 118 | fn litstr(value: &str) -> LitStr { 119 | LitStr::new(value, Span::call_site()) 120 | } 121 | -------------------------------------------------------------------------------- /serdev_derive/src/internal/reexport.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use syn::{parse::Parse, punctuated::Punctuated, token, Attribute, Error, LitStr, MacroDelimiter, Meta, MetaList, Path}; 3 | 4 | 5 | pub(crate) struct Reexport { 6 | path: LitStr, 7 | } 8 | 9 | impl Parse for Reexport { 10 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 11 | let _path: token::Crate = input.parse()?; 12 | 13 | let _eq: token::Eq = input.parse()?; 14 | 15 | let path: LitStr = input.parse()?; 16 | 17 | Ok(Self { path }) 18 | } 19 | } 20 | 21 | impl Reexport { 22 | pub(crate) fn take(attrs: &mut Vec) -> Result, Error> { 23 | for attr in attrs { 24 | if attr.path().get_ident().is_some_and(|i| i == "serdev") { 25 | let directives = attr.parse_args_with( 26 | Punctuated::::parse_terminated 27 | )?; 28 | for (i, directive) in directives.iter().enumerate() { 29 | if directive.to_string().starts_with("crate") { 30 | attr.meta = Meta::List(MetaList { 31 | path: syn::parse_str("serdev")?, 32 | delimiter: MacroDelimiter::Paren(Default::default()), 33 | tokens: syn::parse_str(&{ 34 | let mut others = String::new(); 35 | for (j, directive) in directives.iter().enumerate() { 36 | if j != i { 37 | others.push_str(&directive.to_string()); 38 | others.push(',') 39 | } 40 | }; others.pop(); 41 | others 42 | })? 43 | }); 44 | return syn::parse2(directive.clone()).map(Some) 45 | } 46 | } 47 | } 48 | }; Ok(None) 49 | } 50 | } 51 | 52 | impl Reexport { 53 | pub(crate) fn path(&self) -> Result { 54 | syn::parse_str(&self.path.value()) 55 | } 56 | 57 | pub(crate) fn path_str(&self) -> String { 58 | self.path.value() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /serdev_derive/src/internal/target.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{format_ident, quote, ToTokens}; 3 | use syn::{parse::Parse, Attribute, Error, Fields, Generics, Ident, Item, ItemEnum, ItemStruct}; 4 | 5 | 6 | #[derive(Clone)] 7 | pub(crate) enum Target { 8 | Enum(ItemEnum), 9 | Struct(ItemStruct) 10 | } 11 | 12 | impl Parse for Target { 13 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 14 | match input.parse::()? { 15 | Item::Enum(e) => Ok(Self::Enum(e)), 16 | Item::Struct(s) => Ok(Self::Struct(s)), 17 | _ => Err(Error::new(Span::call_site(), "")) 18 | } 19 | } 20 | } 21 | 22 | impl ToTokens for Target { 23 | fn to_tokens(&self, tokens: &mut TokenStream) { 24 | match self { 25 | Self::Enum(e) => e.to_tokens(tokens), 26 | Self::Struct(s) => s.to_tokens(tokens) 27 | } 28 | } 29 | } 30 | 31 | impl Target { 32 | pub(crate) fn generics(&self) -> &Generics { 33 | match self { 34 | Self::Enum(e) => &e.generics, 35 | Self::Struct(s) => &s.generics 36 | } 37 | } 38 | 39 | pub(crate) fn attrs(&self) -> &[Attribute] { 40 | match self { 41 | Self::Enum(e) => &e.attrs, 42 | Self::Struct(s) => &s.attrs 43 | } 44 | } 45 | pub(crate) fn attrs_mut(&mut self) -> &mut Vec { 46 | match self { 47 | Self::Enum(e) => &mut e.attrs, 48 | Self::Struct(s) => &mut s.attrs 49 | } 50 | } 51 | 52 | pub(crate) fn ident(&self) -> &Ident { 53 | match self { 54 | Self::Enum(e) => &e.ident, 55 | Self::Struct(s) => &s.ident 56 | } 57 | } 58 | pub(crate) fn ident_mut(&mut self) -> &mut Ident { 59 | match self { 60 | Self::Enum(e) => &mut e.ident, 61 | Self::Struct(s) => &mut s.ident 62 | } 63 | } 64 | 65 | pub(crate) fn create_proxy(&self, name: Ident) -> Self { 66 | let mut proxy = self.clone(); 67 | 68 | *proxy.ident_mut() = name; 69 | 70 | *proxy.attrs_mut() = proxy.attrs().iter() 71 | .filter(|a| a.path().get_ident().is_some_and(|i| i == "serde")) 72 | .cloned().collect(); 73 | match &mut proxy { 74 | Self::Struct(s) => for field in &mut s.fields { 75 | field.attrs = field.attrs.iter() 76 | .filter(|a| a.path().get_ident().is_some_and(|i| i == "serde")) 77 | .cloned().collect(); 78 | } 79 | Self::Enum(e) => for variant in &mut e.variants { 80 | variant.attrs = variant.attrs.iter() 81 | .filter(|a| a.path().get_ident().is_some_and(|i| i == "serde")) 82 | .cloned().collect(); 83 | } 84 | } 85 | 86 | proxy 87 | } 88 | 89 | pub(crate) fn transmute_expr(&self, 90 | variable_ident: &'static str, 91 | target_ident: &Ident 92 | ) -> TokenStream { 93 | let var = Ident::new(variable_ident, Span::call_site()); 94 | 95 | fn constructor(fields: &Fields) -> TokenStream { 96 | match fields { 97 | Fields::Unit => { 98 | quote! {} 99 | } 100 | Fields::Unnamed(u) => { 101 | let idents = (0..u.unnamed.len()).map(|i| format_ident!("field_{i}")); 102 | quote! { 103 | ( #(#idents),* ) 104 | } 105 | } 106 | Fields::Named(n) => { 107 | let idents = n.named.iter().map(|f| f.ident.as_ref().unwrap()); 108 | quote! { 109 | { #(#idents),* } 110 | } 111 | } 112 | } 113 | } 114 | 115 | match self { 116 | Self::Struct(s) => { 117 | let ident = &s.ident; 118 | let constructor = constructor(&s.fields); 119 | quote! {{ 120 | let #ident #constructor = #var; 121 | #target_ident #constructor 122 | }} 123 | } 124 | Self::Enum(e) => { 125 | let ident = &e.ident; 126 | 127 | let arms = e.variants.iter().map(|v| { 128 | let variant = &v.ident; 129 | let fields = constructor(&v.fields); 130 | quote! { 131 | #ident::#variant #fields => #target_ident::#variant #fields 132 | } 133 | }); 134 | 135 | quote! { 136 | match #var { 137 | #(#arms),* 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /serdev_derive/src/internal/validate.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use syn::{parse::Parse, punctuated::Punctuated, spanned::Spanned, token, Attribute, Error, Ident, LitStr, MacroDelimiter, Meta, MetaList, Path}; 3 | 4 | 5 | mod keyword { 6 | syn::custom_keyword! { by } 7 | syn::custom_keyword! { error } 8 | } 9 | 10 | pub(crate) enum Validate { 11 | Eq(LitStr), 12 | Paren { by: LitStr, error: Option }, 13 | } 14 | 15 | impl Parse for Validate { 16 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 17 | let _validate = input.parse::()?; 18 | if _validate != "validate" { 19 | return Err(Error::new(Span::call_site(), "expected `validate`")) 20 | } 21 | 22 | if input.peek(token::Eq) { 23 | input.parse::()?; 24 | let by: LitStr = input.parse()?; 25 | Ok(Validate::Eq(by)) 26 | 27 | } else if input.peek(token::Paren) { 28 | let buf; syn::parenthesized!(buf in input); 29 | let mut by = None; 30 | let mut error = None; 31 | while !buf.is_empty() { 32 | if buf.peek(token::Comma) { 33 | buf.parse::()?; 34 | } else if buf.peek(keyword::by) { 35 | buf.parse::()?; 36 | buf.parse::()?; 37 | by = Some(buf.parse()?) 38 | } else if buf.peek(keyword::error) { 39 | buf.parse::()?; 40 | buf.parse::()?; 41 | error = Some(buf.parse()?) 42 | } else { 43 | let rest = buf.parse::()?; 44 | if !rest.is_empty() { 45 | return Err(Error::new(rest.span(), "expected `by = \"...\"` or `error = \"...\"`")) 46 | } else { 47 | return Err(Error::new(rest.span(), format!("rest: `{}`", rest.to_string()))) 48 | } 49 | } 50 | } 51 | let by = by.ok_or(Error::new(Span::call_site(), "expected `by = \"...\"`"))?; 52 | Ok(Validate::Paren { by, error }) 53 | 54 | } else { 55 | Err(Error::new(Span::call_site(), "expected `validate = \"...\"` or `validate(by = \"...\", error = \"...\")`")) 56 | } 57 | } 58 | } 59 | 60 | impl Validate { 61 | pub(crate) fn take(attrs: &mut Vec) -> Result, Error> { 62 | for attr in attrs { 63 | if attr.path().get_ident().is_some_and(|i| i == "serde") { 64 | let directives = attr.parse_args_with( 65 | Punctuated::::parse_terminated 66 | )?; 67 | for (i, directive) in directives.iter().enumerate() { 68 | if directive.to_string().starts_with("validate") { 69 | attr.meta = Meta::List(MetaList { 70 | path: syn::parse_str("serde")?, 71 | delimiter: MacroDelimiter::Paren(token::Paren::default()), 72 | tokens: syn::parse_str(&{ 73 | let mut others = String::new(); 74 | for (j, directive) in directives.iter().enumerate() { 75 | if j != i { 76 | others.push_str(&directive.to_string()); 77 | others.push(',') 78 | } 79 | }; others.pop(); 80 | others 81 | })? 82 | }); 83 | return syn::parse2(directive.clone()).map(Some) 84 | } 85 | } 86 | } 87 | }; Ok(None) 88 | } 89 | 90 | pub(crate) fn function(&self) -> Result { 91 | syn::parse_str(&match self { 92 | Self::Eq(by) => by, 93 | Self::Paren { by, error:_ } => by 94 | }.value()) 95 | } 96 | 97 | pub(crate) fn error(&self) -> Result, Error> { 98 | match self { 99 | Self::Paren { by:_, error: Some(error) } => syn::parse_str(&error.value()).map(Some), 100 | _ => Ok(None) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /serdev_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | mod internal; 4 | 5 | #[proc_macro_derive(Serialize, attributes(serde, serdev))] 6 | pub fn Serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 7 | internal::Serialize(input.into()) 8 | .unwrap_or_else(syn::Error::into_compile_error) 9 | .into() 10 | } 11 | 12 | #[proc_macro_derive(Deserialize, attributes(serde, serdev))] 13 | pub fn Deserialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 14 | internal::Deserialize(input.into()) 15 | .unwrap_or_else(syn::Error::into_compile_error) 16 | .into() 17 | } 18 | 19 | #[doc(hidden)] 20 | #[proc_macro_attribute] 21 | pub fn consume(_: proc_macro::TokenStream, _: proc_macro::TokenStream) -> proc_macro::TokenStream { 22 | proc_macro::TokenStream::new() 23 | } 24 | --------------------------------------------------------------------------------