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
/*!
# Ledger-rs library

Ledger-cli functionality implemented in Rust

Still at a very stage. Early work-in-progress.

The basic functionality demo:

Given a `basic.ledger` text file, with the contents

```
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 basic.ledger");

    assert!(!actual.is_empty());
    assert_eq!(5, actual.len());
    assert_eq!("Account  has balance ", actual[0]);
    assert_eq!("Account Assets has balance ", actual[1]);
    assert_eq!("Account Assets:Cash has balance -20 EUR", actual[2]);
    assert_eq!("Account Expenses has balance ", 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;

/// Account definition and operations
pub mod account;
/// Amount and the Decimal numeric type
pub mod amount;
mod balance;
pub mod commodity;
pub mod history;
pub mod journal;
mod option;
pub mod parser;
mod pool;
pub mod post;
pub mod report;
pub mod scanner;
pub mod utilities;
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).collect::<Vec<String>>();
            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);
}

#[cfg(test)]
mod lib_tests {
    use std::assert_eq;

    use crate::{amount::Amount, option, run, pool::CommodityIndex};

    #[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 
Account Assets has balance -20
Account Expenses has balance 20"#;

        let actual = run(args).join("\n");

        // Assert
        assert!(!actual.is_empty());
        assert_eq!(expected, actual);
    }

    #[test]
    fn test_multiple_files() {
        let args =
            shell_words::split("accounts -f tests/minimal.ledger -f tests/basic.ledger").unwrap();
        let (_commands, input_options) = option::process_arguments(args);

        let journal = super::session_read_journal_files(&input_options);

        // Assert

        // xacts
        assert_eq!(2, journal.xacts.len());
        assert_eq!("Payee", journal.xacts[0].payee);
        assert_eq!("Supermarket", journal.xacts[1].payee);

        // posts
        assert_eq!(4, journal.posts.len());
        assert_eq!(Some(Amount::new(20.into(), None)), journal.posts[0].amount);
        assert_eq!(
            Some(Amount::new(20.into(), Some(CommodityIndex::new(0)))),
            journal.posts[2].amount
        );

        // accounts
        assert_eq!("", journal.accounts[0].name);
        assert_eq!("Expenses", journal.accounts[1].name);
        assert_eq!("Assets", journal.accounts[2].name);
        assert_eq!("Food", journal.accounts[3].name);
        assert_eq!("Cash", journal.accounts[4].name);

        // commodities
        assert_eq!(1, journal.commodity_pool.commodities.len());
        assert_eq!("EUR", journal.commodity_pool.find_commodity("EUR").unwrap().symbol);
    }
}