use std::{array::TryFromSliceError, ops::Deref};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{
errors::GeoCacheError,
geo::{convert_coords_into_microdeg, convert_lang_to_u16, convert_u16_to_lang},
};
#[derive(Debug, Serialize, Hash, Eq, PartialEq)]
pub struct CacheKeyRaw(pub [u8; 20]);
impl<'de> Deserialize<'de> for CacheKeyRaw {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = s.as_bytes();
if bytes.len() != 20 {
return Err(serde::de::Error::custom(
GeoCacheError::CacheKeyRawInvalidLength { len: bytes.len() },
));
}
if !bytes
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'-')
{
return Err(serde::de::Error::custom(
GeoCacheError::CacheKeyRawInvalidCharacters { value: s },
));
}
let mut buf = [0u8; 20];
buf.copy_from_slice(bytes);
Ok(CacheKeyRaw(buf))
}
}
impl Deref for CacheKeyRaw {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub struct CacheKey {
lat: i32,
lng: i32,
lang: u16,
}
impl CacheKey {
pub fn try_new(lat: f64, lng: f64, lang: &str) -> Result<Self, GeoCacheError> {
let (lat, lng) = convert_coords_into_microdeg(lat, lng)?;
let lang_as_u16 = convert_lang_to_u16(lang)?;
Ok(Self {
lat,
lng,
lang: lang_as_u16,
})
}
}
impl TryFrom<&[u8]> for CacheKeyRaw {
type Error = TryFromSliceError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Ok(CacheKeyRaw(value.try_into()?))
}
}
impl From<CacheKeyRaw> for CacheKey {
fn from(value: CacheKeyRaw) -> Self {
let lang = std::str::from_utf8(&value[0..2]).unwrap();
let lat: i32 = std::str::from_utf8(&value[3..11]).unwrap().parse().unwrap();
let lng: i32 = std::str::from_utf8(&value[12..20])
.unwrap()
.parse()
.unwrap();
CacheKey {
lat,
lng,
lang: convert_lang_to_u16(lang).unwrap(),
}
}
}
impl From<CacheKey> for CacheKeyRaw {
fn from(value: CacheKey) -> CacheKeyRaw {
let mut bytes = [0u8; 20];
let lang = convert_u16_to_lang(value.lang).unwrap();
let s = format!("{};{:08};{:08}", lang, value.lat, value.lng);
bytes.copy_from_slice(s.as_bytes());
CacheKeyRaw(bytes)
}
}
#[cfg(test)]
mod tests {
use crate::{cache_key::CacheKey, errors::GeoCacheError};
#[test]
fn test_cache_key_try_new_valid() {
let key = CacheKey::try_new(48.1645819, 17.1847104, "sk");
assert!(key.is_ok());
}
#[test]
fn test_cache_key_try_new_boundary_latitude_min() {
let key = CacheKey::try_new(-90.0, 17.1847104, "sk");
assert!(key.is_ok());
}
#[test]
fn test_cache_key_try_new_boundary_latitude_max() {
let key = CacheKey::try_new(90.0, 17.1847104, "sk");
assert!(key.is_ok());
}
#[test]
fn test_cache_key_try_new_boundary_longitude_min() {
let key = CacheKey::try_new(48.1645819, -180.0, "sk");
assert!(key.is_ok());
}
#[test]
fn test_cache_key_try_new_boundary_longitude_max() {
let key = CacheKey::try_new(48.1645819, 180.0, "sk");
assert!(key.is_ok());
}
#[test]
fn test_cache_key_try_new_latitude_just_above_max() {
let err = CacheKey::try_new(90.000001, 17.1847104, "sk").unwrap_err();
assert!(matches!(err, GeoCacheError::LatitudeOutOfRange { value } if value == 90.000001));
}
#[test]
fn test_cache_key_try_new_latitude_just_below_min() {
let err = CacheKey::try_new(-90.000001, 17.1847104, "sk").unwrap_err();
assert!(matches!(err, GeoCacheError::LatitudeOutOfRange { value } if value == -90.000001));
}
#[test]
fn test_cache_key_try_new_longitude_just_above_max() {
let err = CacheKey::try_new(48.1645819, 180.000001, "sk").unwrap_err();
assert!(matches!(err, GeoCacheError::LongitudeOutOfRange { value } if value == 180.000001));
}
#[test]
fn test_cache_key_try_new_longitude_just_below_min() {
let err = CacheKey::try_new(48.1645819, -180.000001, "sk").unwrap_err();
assert!(matches!(err, GeoCacheError::LongitudeOutOfRange { value } if value == -180.000001));
}
#[test]
fn test_cache_key_try_new_lang_too_short() {
let err = CacheKey::try_new(48.1645819, 17.1847104, "s").unwrap_err();
assert!(matches!(
err,
GeoCacheError::CountryCodeWrongLength { len: 1 }
));
}
#[test]
fn test_cache_key_try_new_lang_empty() {
let err = CacheKey::try_new(48.1645819, 17.1847104, "").unwrap_err();
assert!(matches!(
err,
GeoCacheError::CountryCodeWrongLength { len: 0 }
));
}
#[test]
fn test_cache_key_try_new_lang_too_long() {
let err = CacheKey::try_new(48.1645819, 17.1847104, "SVKK").unwrap_err();
assert!(matches!(
err,
GeoCacheError::CountryCodeWrongLength { len: 4 }
));
}
#[test]
fn test_cache_key_try_new_lang_numeric() {
let err = CacheKey::try_new(48.1645819, 17.1847104, "42").unwrap_err();
assert!(matches!(err, GeoCacheError::InvalidCountryCode { ref code } if code == "42"));
}
#[test]
fn test_cache_key_try_new_lang_special_chars() {
let err = CacheKey::try_new(48.1645819, 17.1847104, "s!").unwrap_err();
assert!(matches!(err, GeoCacheError::InvalidCountryCode { ref code } if code == "s!"));
}
#[test]
fn test_cache_key_try_new_lat_error_before_lng() {
let err = CacheKey::try_new(95.0, 200.0, "sk").unwrap_err();
assert!(matches!(err, GeoCacheError::LatitudeOutOfRange { .. }));
}
#[test]
fn test_cache_key_try_new_coord_error_before_lang() {
let err = CacheKey::try_new(95.0, 17.1847104, "SVK").unwrap_err();
assert!(matches!(err, GeoCacheError::LatitudeOutOfRange { .. }));
}
}