use rand::Rng;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Address {
pub cep: String,
pub logradouro: String,
pub complemento: String,
pub bairro: String,
pub localidade: String,
pub uf: String,
pub ibge: String,
#[serde(default)]
pub gia: String,
pub ddd: String,
pub siafi: String,
}
#[derive(Debug)]
pub struct InvalidCEP {
pub cep: String,
}
impl fmt::Display for InvalidCEP {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CEP '{}' is invalid.", self.cep)
}
}
impl Error for InvalidCEP {}
#[derive(Debug)]
pub struct CEPNotFound {
pub message: String,
}
impl fmt::Display for CEPNotFound {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for CEPNotFound {}
pub fn remove_symbols(dirty: &str) -> String {
dirty.chars().filter(|c| *c != '.' && *c != '-').collect()
}
pub fn format_cep(cep: &str) -> Option<String> {
if is_valid(cep) {
Some(format!("{}-{}", &cep[0..5], &cep[5..8]))
} else {
None
}
}
pub fn is_valid(cep: &str) -> bool {
cep.len() == 8 && cep.chars().all(|c| c.is_ascii_digit())
}
pub fn generate() -> String {
let mut rng = rand::thread_rng();
(0..8).map(|_| rng.gen_range(0..=9).to_string()).collect()
}
pub fn get_address_from_cep(
cep: &str,
raise_exceptions: bool,
) -> Result<Option<Address>, Box<dyn Error>> {
let base_api_url = "https://viacep.com.br/ws/{}/json/";
let clean_cep = remove_symbols(cep);
let cep_is_valid = is_valid(&clean_cep);
if !cep_is_valid {
if raise_exceptions {
return Err(Box::new(InvalidCEP {
cep: cep.to_string(),
}));
}
return Ok(None);
}
let url = base_api_url.replace("{}", &clean_cep);
match reqwest::blocking::get(&url) {
Ok(response) => {
let json: serde_json::Value = response.json()?;
if json.get("erro").is_some() {
if raise_exceptions {
return Err(Box::new(CEPNotFound {
message: cep.to_string(),
}));
}
return Ok(None);
}
let address: Address = serde_json::from_value(json)?;
Ok(Some(address))
}
Err(_e) => {
if raise_exceptions {
return Err(Box::new(CEPNotFound {
message: cep.to_string(),
}));
}
Ok(None)
}
}
}
pub fn get_cep_information_from_address(
federal_unit: &str,
city: &str,
street: &str,
raise_exceptions: bool,
) -> Result<Option<Vec<Address>>, Box<dyn Error>> {
const VALID_UFS: &[&str] = &[
"AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB",
"PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO",
];
let federal_unit_upper = federal_unit.to_uppercase();
if !VALID_UFS.contains(&federal_unit_upper.as_str()) {
if raise_exceptions {
return Err(format!("Invalid UF: {}", federal_unit).into());
}
return Ok(None);
}
let base_api_url = "https://viacep.com.br/ws/{}/{}/{}/json/";
let parsed_city = normalize_string(city);
let parsed_street = normalize_string(street);
let url = base_api_url
.replace("{}", &federal_unit_upper)
.replacen("{}", &parsed_city, 1)
.replacen("{}", &parsed_street, 1);
match reqwest::blocking::get(&url) {
Ok(response) => {
let addresses: Vec<Address> = response.json()?;
if addresses.is_empty() {
if raise_exceptions {
return Err(Box::new(CEPNotFound {
message: format!("{} - {} - {}", federal_unit, city, street),
}));
}
return Ok(None);
}
Ok(Some(addresses))
}
Err(_e) => {
if raise_exceptions {
return Err(Box::new(CEPNotFound {
message: format!("{} - {} - {}", federal_unit, city, street),
}));
}
Ok(None)
}
}
}
fn normalize_string(s: &str) -> String {
use unicode_normalization::UnicodeNormalization;
s.nfd()
.filter(|c| !unicode_normalization::char::is_combining_mark(*c))
.collect::<String>()
.replace(" ", "%20")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remove_symbols() {
assert_eq!(remove_symbols("00000000"), "00000000");
assert_eq!(remove_symbols("01310-200"), "01310200");
assert_eq!(remove_symbols("01..310.-200.-"), "01310200");
assert_eq!(remove_symbols("abc01310200*!*&#"), "abc01310200*!*&#");
assert_eq!(
remove_symbols("ab.c1.--.3-102.-0-.0-.*.-!*&#"),
"abc1310200*!*&#"
);
assert_eq!(remove_symbols("...---..."), "");
}
#[test]
fn test_is_valid() {
assert!(!is_valid("1"));
assert!(!is_valid("12345"));
assert!(!is_valid("123456789"));
assert!(!is_valid("1234567-"));
assert!(!is_valid("abcdefgh"));
assert!(!is_valid("1234567a"));
assert!(is_valid("99999999"));
assert!(is_valid("88390000"));
assert!(is_valid("01310200"));
assert!(is_valid("12345678"));
assert!(is_valid("00000000"));
}
#[test]
fn test_format_cep() {
assert_eq!(format_cep("01310200"), Some("01310-200".to_string()));
assert_eq!(format_cep("12345678"), Some("12345-678".to_string()));
assert_eq!(format_cep("00000000"), Some("00000-000".to_string()));
assert_eq!(format_cep("99999999"), Some("99999-999".to_string()));
assert_eq!(format_cep("12345"), None);
assert_eq!(format_cep("013102009"), None);
assert_eq!(format_cep("abcdefgh"), None);
assert_eq!(format_cep("1234567-"), None);
assert_eq!(format_cep(""), None);
}
#[test]
fn test_generate() {
for _ in 0..1000 {
let cep = generate();
assert_eq!(cep.len(), 8);
assert!(is_valid(&cep));
assert!(cep.chars().all(|c| c.is_ascii_digit()));
}
}
#[test]
fn test_normalize_string() {
assert_eq!(normalize_string("São Paulo"), "Sao%20Paulo");
assert_eq!(normalize_string("Brasília"), "Brasilia");
assert_eq!(normalize_string("Goiânia"), "Goiania");
assert_eq!(normalize_string("Belo Horizonte"), "Belo%20Horizonte");
assert_eq!(normalize_string("Rio de Janeiro"), "Rio%20de%20Janeiro");
assert_eq!(normalize_string("João Pessoa"), "Joao%20Pessoa");
}
#[test]
fn test_remove_symbols_empty() {
assert_eq!(remove_symbols(""), "");
}
#[test]
fn test_remove_symbols_only_symbols() {
assert_eq!(remove_symbols(".-.-.-"), "");
assert_eq!(remove_symbols("..."), "");
assert_eq!(remove_symbols("---"), "");
}
#[test]
fn test_is_valid_edge_cases() {
assert!(!is_valid(""));
assert!(is_valid("00000000"));
assert!(is_valid("99999999"));
}
#[test]
fn test_format_cep_with_cleaned_input() {
assert_eq!(format_cep("01310-200"), None); }
}