#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResidencyTag(u16);
impl ResidencyTag {
pub const GLOBAL: ResidencyTag = ResidencyTag(0);
pub const INDONESIA: ResidencyTag = ResidencyTag(360);
pub const MALAYSIA: ResidencyTag = ResidencyTag(458);
pub const PHILIPPINES: ResidencyTag = ResidencyTag(608);
pub const VIETNAM: ResidencyTag = ResidencyTag(704);
pub const SINGAPORE: ResidencyTag = ResidencyTag(702);
const EXTENSION_BIT: u16 = 0x8000;
pub const fn from_country_code(code: u16) -> Option<ResidencyTag> {
if code & Self::EXTENSION_BIT != 0 {
None } else {
Some(ResidencyTag(code))
}
}
pub const fn from_be_bytes(bytes: [u8; 2]) -> ResidencyTag {
ResidencyTag(u16::from_be_bytes(bytes))
}
pub const fn to_be_bytes(self) -> [u8; 2] {
self.0.to_be_bytes()
}
pub const fn raw(self) -> u16 {
self.0
}
pub const fn is_extended(self) -> bool {
self.0 & Self::EXTENSION_BIT != 0
}
pub const fn country_code(self) -> u16 {
self.0 & !Self::EXTENSION_BIT
}
pub const fn is_global(self) -> bool {
self.0 == 0
}
pub const fn allows_destination(self, dest: ResidencyTag) -> bool {
if self.is_global() {
true
} else if dest.is_global() {
false
} else {
self.country_code() == dest.country_code()
}
}
}
impl core::fmt::Debug for ResidencyTag {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::GLOBAL => write!(f, "ResidencyTag::GLOBAL"),
Self::INDONESIA => write!(f, "ResidencyTag::INDONESIA"),
Self::MALAYSIA => write!(f, "ResidencyTag::MALAYSIA"),
Self::PHILIPPINES => write!(f, "ResidencyTag::PHILIPPINES"),
Self::VIETNAM => write!(f, "ResidencyTag::VIETNAM"),
Self::SINGAPORE => write!(f, "ResidencyTag::SINGAPORE"),
tag if tag.is_extended() => {
write!(f, "ResidencyTag::Extended({})", tag.country_code())
}
tag => write!(f, "ResidencyTag({})", tag.raw()),
}
}
}
impl core::fmt::Display for ResidencyTag {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.is_global() {
write!(f, "GLOBAL")
} else if self.is_extended() {
write!(f, "EXT:{}", self.country_code())
} else {
write!(f, "{:03}", self.raw())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_values_match_iso3166() {
assert_eq!(ResidencyTag::GLOBAL.raw(), 0);
assert_eq!(ResidencyTag::INDONESIA.raw(), 360);
assert_eq!(ResidencyTag::MALAYSIA.raw(), 458);
assert_eq!(ResidencyTag::PHILIPPINES.raw(), 608);
assert_eq!(ResidencyTag::VIETNAM.raw(), 704);
assert_eq!(ResidencyTag::SINGAPORE.raw(), 702);
}
#[test]
fn from_country_code_valid() {
let tag = ResidencyTag::from_country_code(360).unwrap();
assert_eq!(tag, ResidencyTag::INDONESIA);
}
#[test]
fn from_country_code_zero_is_global() {
let tag = ResidencyTag::from_country_code(0).unwrap();
assert_eq!(tag, ResidencyTag::GLOBAL);
assert!(tag.is_global());
}
#[test]
fn from_country_code_max_valid() {
let tag = ResidencyTag::from_country_code(0x7FFF).unwrap();
assert_eq!(tag.raw(), 0x7FFF);
assert!(!tag.is_extended());
}
#[test]
fn from_country_code_rejects_extension_bit() {
assert!(ResidencyTag::from_country_code(0x8000).is_none());
assert!(ResidencyTag::from_country_code(0x8001).is_none());
assert!(ResidencyTag::from_country_code(0xFFFF).is_none());
}
#[test]
fn be_bytes_roundtrip() {
let tags = [
ResidencyTag::GLOBAL,
ResidencyTag::INDONESIA,
ResidencyTag::MALAYSIA,
ResidencyTag::PHILIPPINES,
ResidencyTag::VIETNAM,
ResidencyTag::SINGAPORE,
];
for tag in tags {
let bytes = tag.to_be_bytes();
let reconstructed = ResidencyTag::from_be_bytes(bytes);
assert_eq!(reconstructed, tag, "BE roundtrip failed for {:?}", tag);
}
}
#[test]
fn indonesia_be_bytes() {
let bytes = ResidencyTag::INDONESIA.to_be_bytes();
assert_eq!(bytes, [0x01, 0x68]);
}
#[test]
fn global_be_bytes() {
let bytes = ResidencyTag::GLOBAL.to_be_bytes();
assert_eq!(bytes, [0x00, 0x00]);
}
#[test]
fn extension_flag_detection() {
let normal = ResidencyTag::from_be_bytes([0x01, 0x68]); assert!(!normal.is_extended());
let extended = ResidencyTag::from_be_bytes([0x81, 0x68]); assert!(extended.is_extended());
}
#[test]
fn country_code_strips_extension_bit() {
let extended = ResidencyTag::from_be_bytes([0x81, 0x68]);
assert!(extended.is_extended());
assert_eq!(extended.country_code(), 360); }
#[test]
fn non_extended_country_code_unchanged() {
assert_eq!(ResidencyTag::INDONESIA.country_code(), 360);
assert_eq!(ResidencyTag::GLOBAL.country_code(), 0);
}
#[test]
fn is_global() {
assert!(ResidencyTag::GLOBAL.is_global());
assert!(!ResidencyTag::INDONESIA.is_global());
assert!(!ResidencyTag::MALAYSIA.is_global());
let extended_zero = ResidencyTag::from_be_bytes([0x80, 0x00]);
assert!(!extended_zero.is_global());
}
#[test]
fn global_allows_any_destination() {
assert!(ResidencyTag::GLOBAL.allows_destination(ResidencyTag::INDONESIA));
assert!(ResidencyTag::GLOBAL.allows_destination(ResidencyTag::MALAYSIA));
assert!(ResidencyTag::GLOBAL.allows_destination(ResidencyTag::GLOBAL));
}
#[test]
fn same_country_allows() {
assert!(ResidencyTag::INDONESIA.allows_destination(ResidencyTag::INDONESIA));
assert!(ResidencyTag::MALAYSIA.allows_destination(ResidencyTag::MALAYSIA));
}
#[test]
fn different_country_denies() {
assert!(!ResidencyTag::INDONESIA.allows_destination(ResidencyTag::MALAYSIA));
assert!(!ResidencyTag::MALAYSIA.allows_destination(ResidencyTag::INDONESIA));
assert!(!ResidencyTag::INDONESIA.allows_destination(ResidencyTag::SINGAPORE));
assert!(!ResidencyTag::VIETNAM.allows_destination(ResidencyTag::PHILIPPINES));
}
#[test]
fn country_to_global_is_denied_by_default() {
assert!(!ResidencyTag::INDONESIA.allows_destination(ResidencyTag::GLOBAL));
assert!(!ResidencyTag::MALAYSIA.allows_destination(ResidencyTag::GLOBAL));
}
#[test]
fn display_formatting() {
let mut buf = heapless_fmt(ResidencyTag::GLOBAL);
assert_eq!(buf.as_str(), "GLOBAL");
buf = heapless_fmt(ResidencyTag::INDONESIA);
assert_eq!(buf.as_str(), "360");
let tag7 = ResidencyTag::from_country_code(7).unwrap();
buf = heapless_fmt(tag7);
assert_eq!(buf.as_str(), "007");
let ext = ResidencyTag::from_be_bytes([0x81, 0x68]);
buf = heapless_fmt(ext);
assert_eq!(buf.as_str(), "EXT:360");
}
#[test]
fn debug_named_constants() {
let dbg = format_debug(ResidencyTag::INDONESIA);
assert_eq!(dbg.as_str(), "ResidencyTag::INDONESIA");
let dbg = format_debug(ResidencyTag::GLOBAL);
assert_eq!(dbg.as_str(), "ResidencyTag::GLOBAL");
}
#[test]
fn debug_unknown_code() {
let tag = ResidencyTag::from_country_code(999).unwrap();
let dbg = format_debug(tag);
assert_eq!(dbg.as_str(), "ResidencyTag(999)");
}
#[test]
fn debug_extended() {
let ext = ResidencyTag::from_be_bytes([0x81, 0x68]);
let dbg = format_debug(ext);
assert_eq!(dbg.as_str(), "ResidencyTag::Extended(360)");
}
fn heapless_fmt(tag: ResidencyTag) -> FmtBuf {
let mut buf = FmtBuf::new();
core::fmt::Write::write_fmt(&mut buf, format_args!("{}", tag)).unwrap();
buf
}
fn format_debug(tag: ResidencyTag) -> FmtBuf {
let mut buf = FmtBuf::new();
core::fmt::Write::write_fmt(&mut buf, format_args!("{:?}", tag)).unwrap();
buf
}
struct FmtBuf {
buf: [u8; 64],
len: usize,
}
impl FmtBuf {
fn new() -> Self {
FmtBuf {
buf: [0; 64],
len: 0,
}
}
fn as_str(&self) -> &str {
core::str::from_utf8(&self.buf[..self.len]).unwrap()
}
}
impl core::fmt::Write for FmtBuf {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let bytes = s.as_bytes();
let end = self.len + bytes.len();
if end > self.buf.len() {
return Err(core::fmt::Error);
}
self.buf[self.len..end].copy_from_slice(bytes);
self.len = end;
Ok(())
}
}
}