use rustledger_core::{Account, NaiveDate};
use crate::LedgerState;
use crate::error::{ErrorCode, ValidationError};
pub fn push_account_not_open(
account: &Account,
date: NaiveDate,
subject: &str,
errors: &mut Vec<ValidationError>,
) {
errors.push(ValidationError::new(
ErrorCode::AccountNotOpen,
format!("{subject} {account} was never opened"),
date,
));
}
pub fn require_account_open(
state: &LedgerState,
account: &Account,
date: NaiveDate,
subject: &str,
errors: &mut Vec<ValidationError>,
) -> bool {
if state.accounts.contains_key(account) {
return true;
}
push_account_not_open(account, date, subject, errors);
false
}
pub fn validate_account_name(account: &str, account_types: &[String]) -> Option<String> {
if account.is_empty() {
return Some("account name is empty".to_string());
}
let root = account.split(':').next()?;
if !account_types.iter().any(|t| t == root) {
return Some(format!(
"account must start with one of: {}. To use a different root name, \
rename a type via an option, e.g. `option \"name_income\" \"Revenue\"`",
account_types.join(", ")
));
}
if !rustledger_parser::is_valid_account_name(account) {
return Some(format!(
"'{account}' is not a valid account name: expected two or more \
colon-separated components, each starting with an uppercase or \
caseless letter (sub-components may start with a digit), \
containing only letters, digits, and hyphens"
));
}
None }
#[cfg(test)]
mod tests {
use super::validate_account_name;
fn types() -> Vec<String> {
["Assets", "Liabilities", "Equity", "Income", "Expenses"]
.iter()
.map(ToString::to_string)
.collect()
}
#[test]
fn accepts_parser_valid_names() {
let t = types();
assert_eq!(validate_account_name("Assets:Cash", &t), None);
assert_eq!(validate_account_name("Assets:2024-Bonus", &t), None);
assert_eq!(validate_account_name("Assets:US:BofA:Checking", &t), None);
}
#[test]
fn respects_renamed_roots() {
let t = vec!["Activa".to_string(), "Revenue".to_string()];
assert_eq!(validate_account_name("Activa:Kas", &t), None);
assert!(validate_account_name("Assets:Cash", &t).is_some());
}
#[test]
fn rejects_what_the_lexer_rejects() {
let t = types();
assert!(validate_account_name("Assets", &t).is_some());
assert!(validate_account_name("Assets:N\u{2116}1", &t).is_some()); assert!(validate_account_name("Assets:Cash\u{1F600}", &t).is_some()); assert!(validate_account_name("Assets:cash", &t).is_some());
assert!(validate_account_name("", &t).is_some());
assert!(validate_account_name("Assets:", &t).is_some());
assert!(validate_account_name("Assets::Cash", &t).is_some());
}
#[test]
fn digit_start_root_rejected_even_if_renamed() {
let t = vec!["1Assets".to_string()];
assert!(validate_account_name("1Assets:Cash", &t).is_some());
}
}