use crate::errors::ModelError;
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::fmt;
pub type Metadata = BTreeMap<MetadataKey, String>;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MetadataKey(String);
impl MetadataKey {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
if is_valid_metadata_key(&value) {
Ok(Self(value))
} else {
Err(ModelError::InvalidMetadataKey(value))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Borrow<str> for MetadataKey {
fn borrow(&self) -> &str {
&self.0
}
}
impl fmt::Display for MetadataKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AccountName(String);
impl AccountName {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
if is_valid_account_name(&value) {
Ok(Self(value))
} else {
Err(ModelError::InvalidAccountName(value))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AccountName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommodityName(String);
impl CommodityName {
pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
let value = value.into();
if is_valid_commodity_name(&value) {
Ok(Self(value))
} else {
Err(ModelError::InvalidCommodityName(value))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CommodityName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn is_valid_metadata_key(value: &str) -> bool {
!value.is_empty()
&& value
.chars()
.all(|c| c.is_ascii_lowercase() || c == '_' || c == '-')
}
fn is_valid_account_name(value: &str) -> bool {
let mut segments = value.split(':');
let Some(first) = segments.next() else {
return false;
};
if !is_valid_account_first_segment(first) {
return false;
}
segments.all(is_valid_account_other_segment)
}
fn is_valid_account_first_segment(segment: &str) -> bool {
let mut chars = segment.chars();
match chars.next() {
Some(c) if c.is_ascii_uppercase() => {}
_ => return false,
}
chars.all(is_account_char)
}
fn is_valid_account_other_segment(segment: &str) -> bool {
!segment.is_empty() && segment.chars().all(is_account_char)
}
fn is_account_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn is_valid_commodity_name(value: &str) -> bool {
let chars: Vec<char> = value.chars().collect();
let len = chars.len();
if !(2..=24).contains(&len) {
return false;
}
if !chars[0].is_ascii_uppercase() {
return false;
}
let last = chars[len - 1];
if !(last.is_ascii_uppercase() || last.is_ascii_digit()) {
return false;
}
chars[1..len - 1].iter().all(|&c| {
c.is_ascii_uppercase() || c.is_ascii_digit() || matches!(c, '\'' | '.' | '_' | '-')
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn account_name_accepts_valid_values() {
assert!(AccountName::new("Actifs").is_ok());
assert!(AccountName::new("Actifs:Compte-Joint").is_ok());
assert!(AccountName::new("Actifs:Compte_Joint:sous2").is_ok());
}
#[test]
fn account_name_rejects_invalid_values() {
assert!(AccountName::new("").is_err());
assert!(AccountName::new("actifs").is_err());
assert!(AccountName::new("Actifs:").is_err());
assert!(AccountName::new(":Actifs").is_err());
assert!(AccountName::new("Actifs::Compte").is_err());
assert!(AccountName::new("Actifs Compte").is_err());
}
#[test]
fn commodity_name_accepts_valid_values() {
assert!(CommodityName::new("EUR").is_ok());
assert!(CommodityName::new("BTC.SAT-2").is_ok());
assert!(CommodityName::new("BRK'A").is_ok());
assert!(CommodityName::new("AB").is_ok());
}
#[test]
fn commodity_name_rejects_invalid_values() {
assert!(CommodityName::new("").is_err());
assert!(CommodityName::new("A").is_err()); assert!(CommodityName::new("eur").is_err());
assert!(CommodityName::new("1EUR").is_err());
assert!(CommodityName::new("EU R").is_err());
assert!(CommodityName::new("EUR-").is_err()); assert!(CommodityName::new(format!("A{}", "B".repeat(24))).is_err()); }
#[test]
fn metadata_key_accepts_valid_values() {
assert!(MetadataKey::new("note").is_ok());
assert!(MetadataKey::new("opened_on").is_ok());
assert!(MetadataKey::new("some-key").is_ok());
}
#[test]
fn metadata_key_rejects_invalid_values() {
assert!(MetadataKey::new("").is_err());
assert!(MetadataKey::new("Note").is_err());
assert!(MetadataKey::new("note:").is_err());
assert!(MetadataKey::new("note key").is_err());
}
}