use crate::InternedStr;
#[cfg(feature = "rkyv")]
use crate::intern::AsInternedStr;
macro_rules! domain_newtype {
($name:ident, $kind:literal) => {
#[doc = concat!("Domain-typed identifier for a ", $kind, ". See the [module docs](crate::identifiers) for rationale.")]
#[derive(Debug, Clone, Eq)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[repr(transparent)]
pub struct $name(
#[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))] InternedStr,
);
impl $name {
#[must_use]
pub fn new(s: impl Into<InternedStr>) -> Self {
Self(s.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub const fn as_interned(&self) -> &InternedStr {
&self.0
}
#[must_use]
pub fn into_interned(self) -> InternedStr {
self.0
}
#[must_use]
pub fn ptr_eq(&self, other: &Self) -> bool {
self.0.ptr_eq(&other.0)
}
pub const fn as_interned_mut(&mut self) -> &mut InternedStr {
&mut self.0
}
}
impl PartialEq for $name {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.0 == *other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.0 == **other
}
}
impl PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
impl PartialEq<InternedStr> for $name {
fn eq(&self, other: &InternedStr) -> bool {
self.0 == *other
}
}
impl PartialEq<$name> for &str {
fn eq(&self, other: &$name) -> bool {
other.0 == **self
}
}
impl PartialEq<$name> for str {
fn eq(&self, other: &$name) -> bool {
other.0 == *self
}
}
impl PartialEq<$name> for InternedStr {
fn eq(&self, other: &$name) -> bool {
*self == other.0
}
}
impl std::hash::Hash for $name {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl std::cmp::PartialOrd for $name {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for $name {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.cmp(&other.0)
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &str {
self.0.as_str()
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl std::borrow::Borrow<str> for $name {
fn borrow(&self) -> &str {
self.0.as_str()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
Self(InternedStr::from(s))
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
Self(InternedStr::from(s))
}
}
impl From<&String> for $name {
fn from(s: &String) -> Self {
Self(InternedStr::from(s.as_str()))
}
}
impl From<InternedStr> for $name {
fn from(s: InternedStr) -> Self {
Self(s)
}
}
impl From<&InternedStr> for $name {
fn from(s: &InternedStr) -> Self {
Self(s.clone())
}
}
impl From<&$name> for $name {
fn from(s: &$name) -> Self {
s.clone()
}
}
impl Default for $name {
fn default() -> Self {
Self(InternedStr::default())
}
}
impl serde::Serialize for $name {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
Ok(Self(InternedStr::deserialize(deserializer)?))
}
}
};
}
domain_newtype!(Account, "beancount account name (e.g. `Assets:Cash:USD`)");
domain_newtype!(Currency, "currency code (e.g. `USD`, `EUR`, `AAPL`)");
domain_newtype!(Tag, "beancount tag (e.g. `#travel`)");
domain_newtype!(Link, "beancount link (e.g. `^invoice-2024-01`)");
#[must_use]
pub fn is_subaccount_or_equal(child: &str, parent: &str) -> bool {
if child == parent {
return true;
}
let parent_len = parent.len();
child.len() > parent_len && child.as_bytes()[parent_len] == b':' && child.starts_with(parent)
}
pub const ACCOUNT_TYPES: [&str; 5] = ["Assets", "Liabilities", "Equity", "Income", "Expenses"];
#[must_use]
pub fn account_type(account: &str) -> &'static str {
match account.split(':').next() {
Some("Assets") => "assets",
Some("Liabilities") => "liabilities",
Some("Equity") => "equity",
Some("Income") => "income",
Some("Expenses") => "expenses",
_ => "unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn account_type_classifies_roots_and_unknown() {
assert_eq!(account_type("Assets:Bank:Checking"), "assets");
assert_eq!(account_type("Liabilities:CC"), "liabilities");
assert_eq!(account_type("Equity:Opening"), "equity");
assert_eq!(account_type("Income:Salary"), "income");
assert_eq!(account_type("Expenses:Food"), "expenses");
assert_eq!(account_type("Assets"), "assets");
assert_eq!(account_type("Frobnicate:X"), "unknown");
assert_eq!(account_type(""), "unknown");
assert_eq!(account_type("assets:bank"), "unknown");
}
#[test]
fn test_construction_from_str() {
let a = Account::from("Assets:Bank");
let c = Currency::from("USD");
assert_eq!(a, "Assets:Bank");
assert_eq!(c, "USD");
}
#[test]
fn test_eq_against_str_in_both_directions() {
let a = Account::from("Assets:Bank");
assert_eq!(a, "Assets:Bank");
assert_eq!("Assets:Bank", a);
assert_ne!(a, "Assets:Other");
}
#[test]
fn test_eq_against_self_kind() {
let a1 = Account::from("Assets:Bank");
let a2 = Account::from("Assets:Bank");
let a3 = Account::from("Assets:Other");
assert_eq!(a1, a2);
assert_ne!(a1, a3);
}
#[test]
fn test_hash_borrow_str() {
use std::collections::HashMap;
let mut m: HashMap<Account, u32> = HashMap::new();
m.insert(Account::from("Assets:Bank"), 1);
assert_eq!(m.get("Assets:Bank"), Some(&1));
assert_eq!(m.get("Assets:Other"), None);
}
#[test]
fn test_deref_str_methods() {
let a = Account::from("Assets:Bank:Checking");
assert!(a.starts_with("Assets:"));
assert!(a.contains(':'));
assert_eq!(a.len(), 20);
}
#[test]
fn test_round_trip_interned() {
let i = InternedStr::from("USD");
let c = Currency::from(i.clone());
assert_eq!(c.as_interned(), &i);
assert_eq!(c.into_interned(), i);
}
#[test]
fn test_different_newtypes_dont_cross() {
fn want_account(_: Account) {}
fn want_currency(_: Currency) {}
want_account(Account::from("Assets:X"));
want_currency(Currency::from("USD"));
}
#[test]
fn test_serde_roundtrip() {
let a = Account::from("Assets:Bank");
let json = serde_json::to_string(&a).unwrap();
assert_eq!(json, "\"Assets:Bank\"");
let back: Account = serde_json::from_str(&json).unwrap();
assert_eq!(a, back);
}
#[test]
fn is_subaccount_or_equal_exact_match() {
assert!(is_subaccount_or_equal("Assets:Bank", "Assets:Bank"));
}
#[test]
fn is_subaccount_or_equal_proper_subaccount() {
assert!(is_subaccount_or_equal(
"Assets:Bank:Checking",
"Assets:Bank"
));
assert!(is_subaccount_or_equal(
"Assets:Bank:Checking:Joint",
"Assets:Bank"
));
}
#[test]
fn is_subaccount_or_equal_prefix_without_segment_boundary_excluded() {
assert!(!is_subaccount_or_equal("Assets:BankAlias", "Assets:Bank"));
assert!(!is_subaccount_or_equal(
"Assets:BankAlias:Checking",
"Assets:Bank"
));
}
#[test]
fn is_subaccount_or_equal_parent_is_prefix_substring_excluded() {
assert!(!is_subaccount_or_equal("Assets:Ban", "Assets:Bank"));
}
#[test]
fn is_subaccount_or_equal_empty_inputs() {
assert!(is_subaccount_or_equal("", ""));
assert!(!is_subaccount_or_equal("Assets:Bank", ""));
assert!(!is_subaccount_or_equal("", "Assets:Bank"));
}
#[test]
fn is_subaccount_or_equal_case_sensitive() {
assert!(!is_subaccount_or_equal("Assets:bank", "Assets:Bank"));
assert!(!is_subaccount_or_equal(
"assets:Bank:Checking",
"Assets:Bank"
));
}
}