├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | args: --all-features 20 | 21 | test: 22 | name: Tests 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | features: 27 | - --all-features 28 | - --no-default-features 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | override: true 36 | - uses: actions-rs/cargo@v1 37 | with: 38 | command: test 39 | args: ${{ matrix.features }} 40 | 41 | fmt: 42 | name: Formatting 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | override: true 51 | - run: rustup component add rustfmt 52 | - uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: --all -- --check 56 | 57 | clippy: 58 | name: Clippy 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | - run: rustup component add clippy 68 | - uses: actions-rs/cargo@v1 69 | with: 70 | command: clippy 71 | args: --all-features -- -D warnings 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpf" 3 | version = "0.3.3" 4 | authors = ["Rodrigo Navarro "] 5 | edition = "2018" 6 | description = "Brazilian CPF parsing, validating and formatting library." 7 | categories = ["no-std"] 8 | keywords = ["cpf", "Brasil", "Brazil"] 9 | documentation = "https://docs.rs/cpf" 10 | license = "MIT" 11 | repository = "https://github.com/reu/cpf-rs" 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [dependencies] 18 | rand = { version = "0.8", optional = true, default-features = false } 19 | serde = { version = "1.0", optional = true, default-features = false } 20 | 21 | [dev-dependencies] 22 | rand = { version = "0.8", default-features = false, features = ["std", "std_rng", "small_rng"] } 23 | 24 | [features] 25 | default = ["std"] 26 | std = ["serde?/std"] 27 | serde = ["dep:serde"] 28 | full = ["serde", "std", "rand"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rodrigo Navarro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpf-rs 2 | 3 | Brazilian CPF parsing, validating and formatting library. 4 | 5 | ```rust 6 | use cpf::Cpf; 7 | 8 | // Use the `valid` function if all you need is validating a CPF number 9 | assert!(cpf::valid("385.211.390-39")); 10 | assert!(cpf::valid("38521139039")); 11 | assert!(!cpf::valid("000.000.000-00")); 12 | 13 | // Parse into a Cpf struct if you need formatting or other metadata 14 | let cpf: Cpf = "38521139039".parse()?; 15 | assert_eq!(format!("{cpf}"), "385.211.390-39"); 16 | assert_eq!(cpf.as_str(), "38521139039"); 17 | assert_eq!(cpf.digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 18 | 19 | // Note that the Cpf struct is guaranteed to always be valid 20 | assert!("000.000.000-00".parse::().is_err()); 21 | assert!(cpf::valid("385.211.390-39".parse::()?)); 22 | ``` 23 | 24 | ## no_std support 25 | 26 | The library makes no dynamic allocation and can be used on no_std 27 | environments by disabling the `std` flag: 28 | 29 | ```toml 30 | [dependencies] 31 | cpf = { version = "0.3", default-features = false } 32 | ``` 33 | 34 | ## Random CPF generation support 35 | 36 | The `rand` feature flag enables random CPF generation: 37 | 38 | ```toml 39 | [dependencies] 40 | cpf = { version = "0.3", features = ["rand"] } 41 | rand = "0.8" 42 | ``` 43 | 44 | ```rust 45 | use cpf::Cpf; 46 | use rand; 47 | use rand::Rng; 48 | 49 | let cpf = rand::thread_rng().gen::(); 50 | ``` 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | 4 | //! # CPF 5 | //! 6 | //! Brazilian CPF parsing, validating and formatting library. 7 | //! 8 | //! ```rust 9 | //! # fn main() -> Result<(), cpf::ParseCpfError> { 10 | //! use cpf::Cpf; 11 | //! 12 | //! // Use the `valid` function if all you need is validating a CPF number 13 | //! assert!(cpf::valid("385.211.390-39")); 14 | //! assert!(cpf::valid("38521139039")); 15 | //! assert!(!cpf::valid("000.000.000-00")); 16 | //! 17 | //! // Parse into a Cpf struct if you need formatting or other metadata 18 | //! let cpf: Cpf = "38521139039".parse()?; 19 | //! assert_eq!(format!("{cpf}"), "385.211.390-39"); 20 | //! assert_eq!(cpf.as_str(), "38521139039"); 21 | //! assert_eq!(cpf.digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 22 | //! 23 | //! // Note that the Cpf struct is guaranteed to always be valid 24 | //! assert!("000.000.000-00".parse::().is_err()); 25 | //! assert!(cpf::valid("385.211.390-39".parse::()?)); 26 | //! # Ok(()) 27 | //! # } 28 | //! ``` 29 | //! 30 | //! ## no_std support 31 | //! 32 | //! The library makes no dinamic allocation and can be used on no_std 33 | //! environments by disabling the `std` flag: 34 | //! 35 | //! ```toml 36 | //! [dependencies] 37 | //! cpf = { version = "0.3", default-features = false } 38 | //! ``` 39 | //! 40 | //! ## Random CPF generation support 41 | //! 42 | //! The `rand` feature flag enables random CPF generation: 43 | //! 44 | //! ```toml 45 | //! [dependencies] 46 | //! cpf = { version = "0.3", features = ["rand"] } 47 | //! rand = "0.8" 48 | //! ``` 49 | //! 50 | //! ```rust 51 | //! # #[cfg(feature = "rand")] { 52 | //! use cpf::Cpf; 53 | //! use rand; 54 | //! use rand::Rng; 55 | //! 56 | //! let cpf = rand::thread_rng().gen::(); 57 | //! # } 58 | //! ``` 59 | 60 | use core::convert::TryFrom; 61 | use core::fmt; 62 | use core::str::{from_utf8_unchecked, FromStr}; 63 | 64 | /// Validates a CPF number. 65 | /// ```rust 66 | /// use cpf; 67 | /// 68 | /// assert!(cpf::valid("385.211.390-39")); 69 | /// assert!(!cpf::valid("000.000.000-00")); 70 | /// ``` 71 | pub fn valid>(cpf: T) -> bool { 72 | parse(cpf).is_ok() 73 | } 74 | 75 | #[derive(Debug, PartialEq, Clone, Hash)] 76 | pub enum ParseCpfError { 77 | InvalidLength, 78 | InvalidChecksum, 79 | InvalidDigit, 80 | } 81 | 82 | /// A valid CPF number. 83 | /// 84 | /// Initialize a `Cpf` from a `&str` or an array of digits: 85 | /// ```rust 86 | /// # fn main() -> Result<(), cpf::ParseCpfError> { 87 | /// # use core::convert::TryFrom; 88 | /// use cpf::Cpf; 89 | /// 90 | /// let cpf1 = "385.211.390-39".parse::()?; 91 | /// let cpf2 = Cpf::try_from([3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9])?; 92 | /// assert_eq!(cpf1, cpf2); 93 | /// # Ok(()) 94 | /// # } 95 | /// ``` 96 | /// 97 | /// Note that the `Cpf` struct can only be initialized after a successfully parse, 98 | /// so it is guaranteed to always be valid. 99 | /// ```rust 100 | /// use cpf::Cpf; 101 | /// 102 | /// let cpf = "000.000.000-00".parse::(); 103 | /// assert!(cpf.is_err()); 104 | /// 105 | /// let cpf = "385.211.390-39".parse::().unwrap(); 106 | /// assert!(cpf::valid(cpf)); 107 | /// ``` 108 | #[derive(PartialEq, Eq, Clone, Copy, Hash)] 109 | pub struct Cpf { 110 | digits: [u8; 11], 111 | } 112 | 113 | impl Cpf { 114 | /// The Cpf digits. 115 | /// ```rust 116 | /// # fn main() -> Result<(), cpf::ParseCpfError> { 117 | /// use cpf::Cpf; 118 | /// 119 | /// let cpf: Cpf = "385.211.390-39".parse()?; 120 | /// assert_eq!(cpf.digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 121 | /// # Ok(()) 122 | /// # } 123 | /// ``` 124 | pub fn digits(&self) -> [u8; 11] { 125 | self.digits.map(|digit| digit - 48) 126 | } 127 | 128 | /// Returns the (unformatted) CPF number. 129 | /// ```rust 130 | /// # fn main() -> Result<(), cpf::ParseCpfError> { 131 | /// use cpf::Cpf; 132 | /// 133 | /// let cpf: Cpf = "385.211.390-39".parse()?; 134 | /// assert_eq!(cpf.as_str(), "38521139039"); 135 | /// # Ok(()) 136 | /// # } 137 | /// ``` 138 | /// 139 | /// Note that even being unformatted, the number will be padded by zeros. 140 | /// ```rust 141 | /// # fn main() -> Result<(), cpf::ParseCpfError> { 142 | /// use cpf::Cpf; 143 | /// 144 | /// let cpf: Cpf = "5120101".parse()?; 145 | /// assert_eq!(cpf.as_str(), "00005120101"); 146 | /// # Ok(()) 147 | /// # } 148 | /// ``` 149 | pub fn as_str(&self) -> &str { 150 | // Safety: all the digits are guaranteed to be in ASCII range 151 | unsafe { from_utf8_unchecked(&self.digits) } 152 | } 153 | 154 | fn from_valid_digits(digits: [u8; 11]) -> Cpf { 155 | Cpf { 156 | digits: digits.map(|digit| digit + 48), 157 | } 158 | } 159 | } 160 | 161 | fn valid_digit(digit: &u8) -> bool { 162 | (0..=9).contains(digit) 163 | } 164 | 165 | fn valid_checksum(digits: &[u8; 11]) -> bool { 166 | if digits.windows(2).all(|d| d[0] == d[1]) { 167 | return false; 168 | } 169 | 170 | [9, 10] 171 | .iter() 172 | .all(|d| digits[*d] == check_digit(&digits[0..*d])) 173 | } 174 | 175 | fn check_digit(digits: &[u8]) -> u8 { 176 | let check_sum = digits 177 | .iter() 178 | .rev() 179 | .zip((2..=12).cycle()) 180 | .map(|(i, n)| (i * n) as usize) 181 | .sum::(); 182 | 183 | match check_sum % 11 { 184 | n if n < 2 => 0, 185 | n => 11 - n as u8, 186 | } 187 | } 188 | 189 | fn parse>(cpf: T) -> Result { 190 | try_from_iter( 191 | cpf.as_ref() 192 | .chars() 193 | .filter_map(|c| c.to_digit(10).map(|d| d as u8)) 194 | .rev(), 195 | ) 196 | } 197 | 198 | fn try_from_iter(iter: impl Iterator) -> Result { 199 | let mut digits: [u8; 11] = [0; 11]; 200 | 201 | for (i, digit) in iter 202 | .map(|digit| { 203 | valid_digit(&digit) 204 | .then_some(digit) 205 | .ok_or(ParseCpfError::InvalidDigit) 206 | }) 207 | .enumerate() 208 | { 209 | if i == 11 { 210 | return Err(ParseCpfError::InvalidLength); 211 | } 212 | 213 | digits[10 - i] = digit?; 214 | } 215 | 216 | if digits 217 | .iter() 218 | .take_while(|digit| **digit == 0) 219 | .take(10) 220 | .count() 221 | > 9 222 | { 223 | return Err(ParseCpfError::InvalidLength); 224 | } 225 | 226 | if !valid_checksum(&digits) { 227 | return Err(ParseCpfError::InvalidChecksum); 228 | } 229 | 230 | Ok(Cpf::from_valid_digits(digits)) 231 | } 232 | 233 | impl AsRef for Cpf { 234 | fn as_ref(&self) -> &str { 235 | self.as_str() 236 | } 237 | } 238 | 239 | impl fmt::Debug for Cpf { 240 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 241 | f.debug_tuple("Cpf") 242 | .field(&format_args!("{}", self)) 243 | .finish() 244 | } 245 | } 246 | 247 | impl fmt::Display for Cpf { 248 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 249 | let digits = self.as_str(); 250 | write!( 251 | f, 252 | "{}.{}.{}-{}", 253 | &digits[0..3], 254 | &digits[3..6], 255 | &digits[6..9], 256 | &digits[9..11] 257 | ) 258 | } 259 | } 260 | 261 | impl FromStr for Cpf { 262 | type Err = ParseCpfError; 263 | 264 | fn from_str(s: &str) -> Result { 265 | parse(s) 266 | } 267 | } 268 | 269 | impl TryFrom<&str> for Cpf { 270 | type Error = ParseCpfError; 271 | 272 | fn try_from(value: &str) -> Result { 273 | parse(value) 274 | } 275 | } 276 | 277 | impl TryFrom<[u8; 11]> for Cpf { 278 | type Error = ParseCpfError; 279 | 280 | fn try_from(value: [u8; 11]) -> Result { 281 | if !value.iter().all(valid_digit) { 282 | Err(ParseCpfError::InvalidDigit) 283 | } else if valid_checksum(&value) { 284 | Ok(Cpf::from_valid_digits(value)) 285 | } else { 286 | Err(ParseCpfError::InvalidChecksum) 287 | } 288 | } 289 | } 290 | 291 | impl TryFrom<&[u8]> for Cpf { 292 | type Error = ParseCpfError; 293 | 294 | fn try_from(value: &[u8]) -> Result { 295 | try_from_iter(value.iter().copied().rev()) 296 | } 297 | } 298 | 299 | impl fmt::Display for ParseCpfError { 300 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 301 | match self { 302 | ParseCpfError::InvalidChecksum => "invalid Cpf checksum".fmt(f), 303 | ParseCpfError::InvalidLength => "invalid Cpf length".fmt(f), 304 | ParseCpfError::InvalidDigit => "invalid Cpf digit".fmt(f), 305 | } 306 | } 307 | } 308 | 309 | #[cfg(feature = "std")] 310 | extern crate std; 311 | #[cfg(feature = "std")] 312 | impl std::error::Error for ParseCpfError {} 313 | 314 | #[cfg(feature = "rand")] 315 | use rand::{ 316 | distributions::{Distribution, Standard, Uniform}, 317 | Rng, 318 | }; 319 | 320 | /// Random CPF generation 321 | /// 322 | /// ```rust 323 | /// # #[cfg(feature = "rand")] { 324 | /// use cpf::Cpf; 325 | /// use rand::Rng; 326 | /// 327 | /// let cpf: Cpf = rand::thread_rng().gen(); 328 | /// # } 329 | /// ``` 330 | #[cfg(feature = "rand")] 331 | #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] 332 | impl Distribution for Standard { 333 | fn sample(&self, rng: &mut R) -> Cpf { 334 | let digit = Uniform::from(0..=9); 335 | let mut digits: [u8; 11] = [0; 11]; 336 | for d in digits.iter_mut().take(9) { 337 | *d = digit.sample(rng); 338 | } 339 | digits[9] = check_digit(&digits[0..9]); 340 | digits[10] = check_digit(&digits[0..10]); 341 | 342 | Cpf::from_valid_digits(digits) 343 | } 344 | } 345 | 346 | #[cfg(test)] 347 | mod tests { 348 | use super::*; 349 | 350 | #[test] 351 | fn it_validates() { 352 | assert!(valid("385.211.390-39")); 353 | assert!(!valid("385.211.390-49")); 354 | } 355 | 356 | #[test] 357 | fn it_disallow_same_digit_numbers() { 358 | assert!(!valid("111.111.111-11")); 359 | assert!(!valid("222.222.222-22")); 360 | assert!(!valid("333.333.333-33")); 361 | assert!(!valid("444.444.444-44")); 362 | assert!(!valid("555.555.555-55")); 363 | assert!(!valid("666.666.666-66")); 364 | assert!(!valid("777.777.777-77")); 365 | assert!(!valid("888.888.888-88")); 366 | assert!(!valid("999.999.999-99")); 367 | } 368 | 369 | #[test] 370 | fn it_parses() { 371 | let cpf = "38521139039".parse::(); 372 | assert!(cpf.is_ok()); 373 | assert_eq!(cpf.unwrap().digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 374 | 375 | let cpf = "385.211.390-39".parse::(); 376 | assert!(cpf.is_ok()); 377 | assert_eq!(cpf.unwrap().digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 378 | } 379 | 380 | #[test] 381 | fn it_initializes_with_digits() { 382 | let cpf = Cpf::try_from([3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 383 | assert!(cpf.is_ok()); 384 | assert_eq!(cpf.unwrap().digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 385 | 386 | let cpf = Cpf::try_from([4, 4, 7, 2, 1, 8, 4, 9, 8, 1, 9]); 387 | assert!(cpf.is_err()); 388 | } 389 | 390 | #[test] 391 | fn it_fails_with_invalid_digits() { 392 | let digits = [3u8, 11, 9, 0, 8 + 22, 8, 5, 6, 33, 3, 2]; 393 | assert_eq!( 394 | Cpf::try_from(digits).unwrap_err(), 395 | ParseCpfError::InvalidDigit 396 | ); 397 | assert_eq!( 398 | Cpf::try_from(&digits[..]).unwrap_err(), 399 | ParseCpfError::InvalidDigit 400 | ); 401 | } 402 | 403 | #[test] 404 | fn it_initializes_with_digits_on_heap() { 405 | extern crate std; 406 | use std::vec; 407 | 408 | let cpf = Cpf::try_from(&vec![3_u8, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9][..]); 409 | assert!(cpf.is_ok()); 410 | assert_eq!(cpf.unwrap().digits(), [3, 8, 5, 2, 1, 1, 3, 9, 0, 3, 9]); 411 | 412 | let cpf = Cpf::try_from(&vec![4_u8, 4, 7, 2, 1, 8, 4, 9, 8, 1, 9][..]); 413 | assert!(cpf.is_err()); 414 | } 415 | 416 | #[test] 417 | fn it_pads_numbers_with_less_than_eleven_digits() { 418 | let cpf = parse("5120101").unwrap(); 419 | assert_eq!(cpf.digits(), [0, 0, 0, 0, 5, 1, 2, 0, 1, 0, 1]); 420 | } 421 | 422 | #[test] 423 | fn it_fails_to_parse_numbers_that_dont_match_the_min_length() { 424 | let cpf = "0".parse::(); 425 | assert!(cpf.is_err()); 426 | assert_eq!(cpf.unwrap_err(), ParseCpfError::InvalidLength); 427 | 428 | let cpf = "01".parse::(); 429 | assert!(cpf.is_err()); 430 | assert_eq!(cpf.unwrap_err(), ParseCpfError::InvalidLength); 431 | 432 | let cpf = "272".parse::(); 433 | assert!(cpf.is_ok()); 434 | } 435 | 436 | #[test] 437 | fn it_fails_to_parse_numbers_with_more_than_eleven_digits() { 438 | let cpf = "138521139039".parse::(); 439 | assert!(cpf.is_err()); 440 | assert_eq!(cpf.unwrap_err(), ParseCpfError::InvalidLength); 441 | } 442 | 443 | #[test] 444 | fn it_fails_on_invalid_checksums() { 445 | let cpf = "38521139038".parse::(); 446 | assert!(cpf.is_err()); 447 | assert_eq!(cpf.unwrap_err(), ParseCpfError::InvalidChecksum); 448 | } 449 | 450 | #[test] 451 | fn it_display() { 452 | extern crate std; 453 | let cpf = "38521139039".parse::().unwrap(); 454 | assert_eq!(std::format!("{}", cpf), "385.211.390-39"); 455 | } 456 | 457 | #[test] 458 | #[cfg(feature = "rand")] 459 | fn it_generates_valid_numbers() { 460 | use rand::{rngs::SmallRng, Rng, SeedableRng}; 461 | let mut rng = SmallRng::seed_from_u64(0); 462 | for _ in 1..10000 { 463 | let cpf = rng.gen::(); 464 | assert!(valid(cpf)); 465 | } 466 | } 467 | 468 | #[test] 469 | #[cfg(feature = "rand")] 470 | fn it_generates_different_numbers() { 471 | use rand::{rngs::SmallRng, Rng, SeedableRng}; 472 | let mut rng = SmallRng::seed_from_u64(0); 473 | let cpf1 = rng.gen::(); 474 | let cpf2 = rng.gen::(); 475 | assert_ne!(cpf1, cpf2); 476 | } 477 | } 478 | 479 | #[cfg(feature = "serde")] 480 | mod __serde { 481 | use core::fmt; 482 | 483 | use serde::de::{Error, Visitor}; 484 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 485 | 486 | use super::*; 487 | 488 | impl<'de> Deserialize<'de> for Cpf { 489 | fn deserialize(deserializer: D) -> Result 490 | where 491 | D: Deserializer<'de>, 492 | { 493 | struct CpfVisitor; 494 | 495 | impl<'de> Visitor<'de> for CpfVisitor { 496 | type Value = Cpf; 497 | 498 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 499 | formatter.write_str("a string containing a valid Cpf") 500 | } 501 | 502 | fn visit_str(self, v: &str) -> Result 503 | where 504 | E: Error, 505 | { 506 | v.parse().map_err(Error::custom) 507 | } 508 | 509 | fn visit_borrowed_str(self, v: &'de str) -> Result 510 | where 511 | E: Error, 512 | { 513 | v.parse().map_err(Error::custom) 514 | } 515 | 516 | #[cfg(feature = "std")] 517 | fn visit_string(self, v: std::string::String) -> Result 518 | where 519 | E: Error, 520 | { 521 | v.parse().map_err(Error::custom) 522 | } 523 | } 524 | 525 | deserializer.deserialize_str(CpfVisitor) 526 | } 527 | } 528 | 529 | impl Serialize for Cpf { 530 | fn serialize(&self, serializer: S) -> Result 531 | where 532 | S: Serializer, 533 | { 534 | self.as_str().serialize(serializer) 535 | } 536 | } 537 | 538 | #[cfg(test)] 539 | mod tests { 540 | use serde::de::value::Error; 541 | use serde::de::IntoDeserializer; 542 | 543 | use super::*; 544 | 545 | #[test] 546 | fn it_deserializes_valid_str() { 547 | use serde::de::value::StrDeserializer; 548 | 549 | let de: StrDeserializer<'_, Error> = "385.211.390-39".into_deserializer(); 550 | assert!(Cpf::deserialize(de).is_ok()); 551 | } 552 | 553 | #[test] 554 | fn it_deserializes_invalid_str() { 555 | use serde::de::value::StrDeserializer; 556 | 557 | let de: StrDeserializer<'_, Error> = "385.211.390-49".into_deserializer(); 558 | assert!(Cpf::deserialize(de).is_err()); 559 | } 560 | 561 | #[test] 562 | #[cfg(feature = "std")] 563 | fn it_deserializes_valid_borrowed_cow() { 564 | use std::borrow::Cow; 565 | 566 | use serde::de::value::CowStrDeserializer; 567 | 568 | let cpf = "385.211.390-39"; 569 | let de: CowStrDeserializer<'_, Error> = Cow::from(cpf).into_deserializer(); 570 | assert!(Cpf::deserialize(de).is_ok()); 571 | } 572 | 573 | #[test] 574 | #[cfg(feature = "std")] 575 | fn it_deserializes_invalid_borrowed_cow() { 576 | use std::borrow::Cow; 577 | 578 | use serde::de::value::CowStrDeserializer; 579 | 580 | let cpf = "385.211.390-49"; 581 | let de: CowStrDeserializer<'_, Error> = Cow::from(cpf).into_deserializer(); 582 | assert!(Cpf::deserialize(de).is_err()); 583 | } 584 | 585 | #[test] 586 | #[cfg(feature = "std")] 587 | fn it_deserializes_valid_owned_cow() { 588 | use std::borrow::Cow; 589 | use std::string::ToString; 590 | 591 | use serde::de::value::CowStrDeserializer; 592 | 593 | let cpf = "385.211.390-39".to_string(); 594 | let de: CowStrDeserializer<'_, Error> = Cow::from(cpf).into_deserializer(); 595 | assert!(Cpf::deserialize(de).is_ok()); 596 | } 597 | 598 | #[test] 599 | #[cfg(feature = "std")] 600 | fn it_deserializes_invalid_owned_cow() { 601 | use std::borrow::Cow; 602 | use std::string::ToString; 603 | 604 | use serde::de::value::CowStrDeserializer; 605 | 606 | let cpf = "385.211.390-49".to_string(); 607 | let de: CowStrDeserializer<'_, Error> = Cow::from(cpf).into_deserializer(); 608 | assert!(Cpf::deserialize(de).is_err()); 609 | } 610 | 611 | #[test] 612 | #[cfg(feature = "std")] 613 | fn it_deserializes_valid_string() { 614 | use std::string::ToString; 615 | 616 | use serde::de::value::StringDeserializer; 617 | 618 | let de: StringDeserializer = "385.211.390-39".to_string().into_deserializer(); 619 | assert!(Cpf::deserialize(de).is_ok()); 620 | } 621 | 622 | #[test] 623 | #[cfg(feature = "std")] 624 | fn it_deserializes_invalid_string() { 625 | use std::string::ToString; 626 | 627 | use serde::de::value::StringDeserializer; 628 | 629 | let de: StringDeserializer = "385.211.390-49".to_string().into_deserializer(); 630 | assert!(Cpf::deserialize(de).is_err()); 631 | } 632 | } 633 | } 634 | --------------------------------------------------------------------------------