1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
/*!
# Ledger-rs library
Ledger-cli functionality implemented in Rust
Early work-in-progress.
The basic functionality demo:
Given a `basic.ledger` text file, with the contents
```ledger
2023-04-21 Supermarket
Expenses:Food 20 EUR
Assets:Cash
```
you can use the library to parse the transactions from the file and provide a basic
report on account balances
```
let actual = ledger_rs_lib::run_command("b -f tests/basic.ledger");
assert!(!actual.is_empty());
assert_eq!(5, actual.len());
assert_eq!("Account has balance 0 EUR", actual[0]);
assert_eq!("Account Assets has balance -20 EUR", actual[1]);
assert_eq!("Account Assets:Cash has balance -20 EUR", actual[2]);
assert_eq!("Account Expenses has balance 20 EUR", actual[3]);
assert_eq!("Account Expenses:Food has balance 20 EUR", actual[4]);
```
*/
use std::{fs::File, io::Cursor};
use journal::Journal;
use option::InputOptions;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
pub mod account;
mod annotate;
pub mod amount;
mod balance;
pub mod commodity;
mod directives;
pub mod history;
mod iterator;
pub mod journal;
mod journalreader;
mod option;
pub mod parser;
pub mod pool;
pub mod post;
pub mod report;
pub mod scanner;
pub mod utilities;
mod value;
pub mod xact;
/// An entry point for the CLIs.
/// The commands and arguments sent to the CLI are processed here. This is
/// so that 3rd-party clients can pass argv and get the same result.
/// The arguments should be compatible with Ledger, so that the functionality is comparable.
///
pub fn run(args: Vec<String>) -> Vec<String> {
// separates commands from the options
let (commands, options) = option::process_arguments(args);
execute_command(commands, options)
}
/// A convenient entry point if you want to use a command string directly.
/// command: &str A Ledger-style command, i.e. "balance -f journal.ledger"
///
pub fn run_command(command: &str) -> Vec<String> {
let args = shell_words::split(command).unwrap();
run(args)
}
/// global::execute_command equivalent
fn execute_command(commands: Vec<String>, input_options: InputOptions) -> Vec<String> {
let verb = commands.iter().nth(0).unwrap();
// todo: look for pre-command
// look_for_precommand(verb);
// if !precommand
// if !at_repl
let journal = session_read_journal_files(&input_options);
// todo: lookup(COMMAND, verb)
let command_args = &commands[1..];
// execute command
match verb.chars().next().unwrap() {
'a' => {
// accounts?
// TODO: replace this temporary report
let mut output = report::report_accounts(&journal);
output.sort();
output
}
'b' => {
match verb.as_str() {
"b" | "bal" | "balance" => {
// balance report
report::balance_report(&journal)
}
"budget" => {
// budget
todo!("budget!")
}
_ => {
todo!("?")
}
}
}
_ => todo!("handle"),
}
}
fn look_for_precommand(verb: &str) {
todo!()
}
fn session_read_journal_files(options: &InputOptions) -> Journal {
// Minimalistic approach:
// get the file input
// multiple filenames
let mut journal = Journal::new();
for filename in &options.filenames {
// parse the journal file(s)
parse_file(filename, &mut journal);
}
journal
}
/// Parse input and return the model structure.
pub fn parse_file(file_path: &str, journal: &mut Journal) {
let file = File::open(file_path).expect("file opened");
parser::read_into_journal(file, journal);
}
/// Parses text containing Ledger-style journal.
/// text: &str A Ledger-style journal. The same content that is normally
/// stored in text files
/// journal: &mut Journal The result are stored in the given Journal instance.
pub fn parse_text(text: &str, journal: &mut Journal) {
let source = Cursor::new(text);
parser::read_into_journal(source, journal);
}
pub fn parser_experiment() {
// read line from the Journal
// determine the type
// DirectiveType
// scan the line
// parse into a model instance
// read additional lines, as needed. Ie for Xact/Posts.
// return the directive with the entity, if created
todo!("try the new approach")
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub fn wasm_test() -> String {
"hello from wasm".to_owned()
}
#[cfg(test)]
mod lib_tests {
use std::assert_eq;
use crate::{amount::{Amount, Quantity}, option, run};
// Try to understand why this test fails when dereferencing.
#[test]
fn test_minimal() {
// create a ledger command
let command = "b -f tests/minimal.ledger";
let args = shell_words::split(command).unwrap();
let expected = r#"Account has balance 0
Account Assets has balance -20
Account Expenses has balance 20"#;
// the test fails when checking the parent in fullname() for
// Assets (the first child account).
let actual = run(args).join("\n");
// Assert
assert!(!actual.is_empty());
assert_eq!(expected, actual);
}
#[test]
fn test_multiple_files() {
// arrange
let args =
shell_words::split("accounts -f tests/minimal.ledger -f tests/basic.ledger").unwrap();
let (_commands, input_options) = option::process_arguments(args);
// let cdty = Commodity::new("EUR");
// Act
let journal = super::session_read_journal_files(&input_options);
// Assert
let xact0 = &journal.xacts[0];
let xact1 = &journal.xacts[1];
// xacts
assert_eq!(2, journal.xacts.len());
assert_eq!("Payee", xact0.payee);
assert_eq!("Supermarket", xact1.payee);
// posts
// assert_eq!(4, journal.posts.len());
assert_eq!(2, xact0.posts.len());
assert_eq!(2, xact1.posts.len());
assert_eq!(Some(Amount::new(20.into(), None)), xact0.posts[0].amount);
// amount
assert_eq!(xact1.posts[0].amount.unwrap().quantity, Quantity::from_str("20").unwrap());
assert_eq!(xact1.posts[0].amount.unwrap().get_commodity().unwrap().symbol, "EUR");
// accounts
let mut accounts = journal.master.flatten_account_tree();
// hack for the test, since the order of items in a hashmap is not guaranteed.
accounts.sort_unstable_by_key(|acc| &acc.name);
assert_eq!("", accounts[0].name);
assert_eq!("Assets", accounts[1].name);
assert_eq!("Cash", accounts[2].name);
assert_eq!("Expenses", accounts[3].name);
assert_eq!("Food", accounts[4].name);
// commodities
assert_eq!(1, journal.commodity_pool.commodities.len());
assert_eq!(
"EUR",
journal.commodity_pool.find("EUR").unwrap().symbol
);
}
}