avaro 0.0.2-alpha

a plain text double-accounting tool which is compatible with avaro but more powerful
Documentation
use crate::{
    models::{Amount, Directive, Flag},
    utils::escape_with_quote,
};
use itertools::Itertools;

pub trait ToAvaroFile {
    fn to_text(&self) -> String;
}

impl ToAvaroFile for Amount {
    fn to_text(&self) -> String {
        format!("{} {}", self.0, self.1)
    }
}

impl ToAvaroFile for crate::models::Flag {
    fn to_text(&self) -> String {
        match self {
            Flag::Complete => "*".to_owned(),
            Flag::Incomplete => "!".to_owned(),
        }
    }
}

impl ToAvaroFile for crate::models::AccountType {
    fn to_text(&self) -> String {
        self.to_string()
    }
}

impl ToAvaroFile for crate::models::TransactionLine {
    fn to_text(&self) -> String {
        let mut builder = String::new();
        if self.flag != Flag::Complete {
            builder.push_str("! ");
        }
        builder.push_str(&self.account.to_string());
        if let Some(amount_inner) = &self.amount {
            builder.push_str(&format!(" {}", amount_inner.to_text()));
        };
        if let Some((amount, note)) = &self.cost {
            builder.push_str(" { ");
            builder.push_str(&amount.to_text());
            if let Some(note_inner) = note {
                builder.push_str(", ");
                builder.push_str(&escape_with_quote(note_inner));
            }
            builder.push_str(" }");
        };
        if let Some(single) = &self.single_price {
            builder.push_str(&format!(" @ {}", single.to_text()));
        };
        if let Some(inner) = &self.total_price {
            builder.push_str(&format!(" @@ {}", inner.to_text()));
        };
        builder
    }
}

impl ToAvaroFile for crate::models::Transaction {
    fn to_text(&self) -> String {
        let mut builder = String::new();
        builder.push_str(&self.date.to_string());
        builder.push_str(" ");
        builder.push_str(&self.flag.to_text());
        let pn = match (&self.payee, &self.narration) {
            (Some(payee), Some(narration)) => format!(
                " {} {}",
                escape_with_quote(payee),
                escape_with_quote(narration)
            ),
            (None, Some(narration)) => format!(" {}", escape_with_quote(narration)),
            _ => format!(""),
        };
        builder.push_str(&pn);

        let tags = self
            .tags
            .iter()
            .map(|inner| format!(" #{}", inner))
            .join("");
        builder.push_str(&tags);
        let links = self
            .links
            .iter()
            .map(|inner| format!(" ^{}", inner))
            .join("");
        builder.push_str(&links);

        let lines = self
            .lines
            .iter()
            .map(|line| format!("\n  {}", line.to_text()))
            .join("");
        builder.push_str(&lines);

        builder
    }
}

impl ToAvaroFile for crate::models::Directive {
    fn to_text(&self) -> String {
        match self {
            Directive::Open {
                date,
                account,
                commodities,
            } => {
                let mut string = format!(
                    "{date} open {account}",
                    date = &date.to_string(),
                    account = &account.to_string()
                );
                if let Some(commodities_data) = commodities {
                    string.push(' ');
                    string.push_str(&commodities_data.iter().join(", "));
                };
                string
            }

            Directive::Close { date, account } => format!(
                "{date} close {account}",
                date = &date.to_string(),
                account = &account.to_string()
            ),
            Directive::Commodity { date, name, metas } => {
                let meta_info = metas
                    .iter()
                    .map(|(key, value)| {
                        format!("\n  {}: {}", key.clone(), escape_with_quote(value))
                    })
                    .join("");
                format!(
                    "{date} commodity {name}{meta_info}",
                    date = &date.to_string(),
                    name = name,
                    meta_info = meta_info
                )
            }
            Directive::Transaction(model) => model.to_text(),
            Directive::Balance {
                date,
                account,
                amount,
            } => format!(
                "{date} balance {account} {amount}",
                date = date.to_string(),
                account = account.to_string(),
                amount = amount.to_text()
            ),
            Directive::Pad { date, from, to } => format!(
                "{date} pad {from} {to}",
                date = date.to_string(),
                from = from.to_string(),
                to = to.to_string()
            ),
            Directive::Note {
                date,
                account,
                description,
            } => format!(
                "{date} note {account} {description}",
                date = date.to_string(),
                account = account.to_string(),
                description = escape_with_quote(description)
            ),
            Directive::Document {
                date,
                account,
                path,
            } => format!(
                "{date} document {account} {path}",
                date = date.to_string(),
                account = account.to_string(),
                path = escape_with_quote(path)
            ),
            Directive::Price {
                date,
                commodity,
                amount,
            } => format!(
                "{date} price {commodity} {amount}",
                date = date.to_string(),
                commodity = commodity,
                amount = amount.to_text()
            ),
            Directive::Event { date, name, value } => format!(
                "{date} event {name} {value}",
                date = date.to_string(),
                name = escape_with_quote(name),
                value = escape_with_quote(value),
            ),
            Directive::Custom {
                date,
                type_name,
                values,
            } => format!(
                "{date} custom {type_name} {value}",
                date = date.to_string(),
                type_name = escape_with_quote(type_name),
                value = values.iter().map(|v| escape_with_quote(v)).join(" ")
            ),
            Directive::Option { key, value } => format!(
                "option {} {}",
                escape_with_quote(key),
                escape_with_quote(value)
            ),
            Directive::Plugin { module, value } => {
                let mut builder = format!("plugin {}", escape_with_quote(module),);
                if let Some(inner) = value {
                    builder.push_str(&format!(" {}", escape_with_quote(inner)));
                }
                builder
            }
            Directive::Include { file } => format!("include {}", escape_with_quote(file)),
            Directive::Comment { content } => content.to_owned(),
        }
    }
}

#[cfg(test)]
mod test {
    use crate::{models::Directive, parser::DirectiveExpressionParser, to_file::ToAvaroFile};

    fn parse(from: &str) -> String {
        let direct: Directive = DirectiveExpressionParser::new().parse(from).unwrap();
        direct.to_text()
    }

    fn parse_and_test(from: &str) {
        assert_eq!(from, parse(from));
    }

    #[test]
    fn open_to_text() {
        parse_and_test("1970-01-01 open Equity:hello CNY");
    }

    #[test]
    fn balance() {
        parse_and_test("1970-01-01 balance Equity:hello 10 CNY");
    }
    #[test]
    fn option() {
        parse_and_test("option \"hello\" \"value\"");
    }
    #[test]
    fn close() {
        parse_and_test("1970-01-01 close Equity:hello");
    }

    #[test]
    fn commodity() {
        parse_and_test("1970-01-01 commodity CNY");
        parse_and_test("1970-01-01 commodity CNY\n  a: \"b\"");
    }

    #[test]
    fn transaction() {
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CNY",
            parse(
                r#"1970-01-01 * "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CNY"#
            )
        );

        assert_eq!(
            "1970-01-01 * \"Narration\"\n  Assets:123 -1 CNY { 0.1 USD, \"TEST\" }\n  Expenses:TestCategory:One 1 CNY { 0.1 USD }",
            parse(r#"1970-01-01 * "Narration"
                  Assets:123  -1 CNY {0.1 USD , "TEST"}
                  Expenses:TestCategory:One 1 CNY {0.1 USD}"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 0.5 CNY\n  Expenses:TestCategory:Two 0.5 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 0.5 CNY
                  Expenses:TestCategory:Two 0.5 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One",
            parse(r#"1970-01-01 * "Payee" "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CCC @ 1 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CCC @ 1 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\"\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CCC @@ 1 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration"
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CCC @@ 1 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Narration\" #mytag #tag2\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CCC @@ 1 CNY",
            parse(r#"1970-01-01 *  "Narration" #mytag #tag2
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CCC @@ 1 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\" #mytag #tag2\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CCC @@ 1 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration" #mytag #tag2
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CCC @@ 1 CNY"#)
        );
        assert_eq!(
            "1970-01-01 * \"Payee\" \"Narration\" ^link1 ^link-2\n  Assets:123 -1 CNY\n  Expenses:TestCategory:One 1 CCC @@ 1 CNY",
            parse(r#"1970-01-01 * "Payee" "Narration" ^link1 ^link-2
                  Assets:123  -1 CNY
                  Expenses:TestCategory:One 1 CCC @@ 1 CNY"#)
        );
    }
    #[test]
    fn pad() {
        parse_and_test("1970-01-01 pad Assets:123:234:English:中文:日本語:한국어 Equity:ABC");
    }
    #[test]
    fn note() {
        parse_and_test(r#"1970-01-01 note Assets:123 "你 好 啊\\""#);
    }
    #[test]
    fn document() {
        parse_and_test("1970-01-01 document Assets:123 \"\"");
        parse_and_test(r#"1970-01-01 document Assets:123 "here I am""#);
    }

    #[test]
    fn price() {
        parse_and_test(r#"1970-01-01 price USD 7 CNY"#);
    }

    #[test]
    fn event() {
        parse_and_test(r#"1970-01-01 event "location" "China""#);
    }
    #[test]
    #[ignore]
    fn custom() {
        parse_and_test(r#"1970-01-01 custom "budget" Expenses:Eat "monthly" CNY"#);
    }

    #[test]
    fn plugin() {
        parse_and_test(r#"plugin "module name" "config data""#);
        parse_and_test(r#"plugin "module name""#);
    }
    #[test]
    fn include() {
        parse_and_test(r#"include "file path""#);
    }
    #[test]
    fn comment() {
        parse_and_test(";你好啊");
    }
}