1
//! Cross-frontend form descriptors and shared validators.
2

            
3
pub use descriptors::{
4
    ACCOUNT_CREATE, COMMODITY_CONVERT, COMMODITY_CREATE, FieldSpec, FormDescriptor, SelectSource,
5
    TRANSACTION_CREATE, WidgetKind,
6
};
7
pub use payload::{build_transaction_logical_payload, build_transaction_update_payload};
8
pub use reverse::{EditableRow, EditableTransaction, parse_editable_transaction};
9
pub use validators::{
10
    LogicalSplitInput, amount_token, now_template, parse_amount, row_complete, step_date,
11
    validate_date,
12
};
13

            
14
mod descriptors;
15
mod payload;
16
pub mod reverse;
17
#[cfg(test)]
18
pub mod tests;
19
mod validators;
20

            
21
/// Serialise a [`FormDescriptor`] to a nomiscript-readable s-expression.
22
8
pub fn to_sexpr(form: &FormDescriptor) -> String {
23
8
    let fields: Vec<String> = form.fields.iter().map(field_to_sexpr).collect();
24
8
    format!("(:form \"{}\" :fields ({}))", form.name, fields.join(" "))
25
8
}
26

            
27
18
fn widget_to_sexpr(w: WidgetKind) -> &'static str {
28
6
    match w {
29
8
        WidgetKind::Text => "text",
30
1
        WidgetKind::Amount => "amount",
31
1
        WidgetKind::Date => "date",
32
1
        WidgetKind::Splits => "splits",
33
1
        WidgetKind::Note => "note",
34
        WidgetKind::Select {
35
            source: SelectSource::Accounts,
36
4
        } => "(select :source accounts)",
37
        WidgetKind::Select {
38
            source: SelectSource::Commodities,
39
2
        } => "(select :source commodities)",
40
    }
41
18
}
42

            
43
18
fn field_to_sexpr(f: &FieldSpec) -> String {
44
18
    format!(
45
        "(:key \"{}\" :label \"{}\" :widget {} :required {})",
46
        f.key,
47
        f.label,
48
18
        widget_to_sexpr(f.widget),
49
18
        if f.required { "t" } else { "nil" }
50
    )
51
18
}
52

            
53
/// Parse a list-accounts wire reply into (id, label) account option pairs.
54
///
55
/// Returns `[]` on any parse failure — never panics.
56
43
pub fn parse_account_options(wire: &str) -> Vec<(String, String)> {
57
    use crate::eval::plist_field;
58
    use crate::render::{WireValue, parse_wire, reparse_list};
59
    use nomiscript::{Value, list_to_vec};
60

            
61
43
    let Ok(WireValue::Value(Value::String(ref s))) = parse_wire(wire) else {
62
2
        return vec![];
63
    };
64
41
    let Ok(list) = reparse_list(s) else {
65
        return vec![];
66
    };
67
41
    let Some(elements) = list_to_vec(&list) else {
68
        return vec![];
69
    };
70
41
    elements
71
41
        .iter()
72
41
        .filter_map(|e| {
73
40
            let id = plist_field(e, ":id")?;
74
40
            let name = plist_field(e, ":name")?;
75
40
            Some((id, name))
76
40
        })
77
41
        .collect()
78
43
}
79

            
80
/// Parse a list-commodities wire reply into (id, label) commodity option pairs.
81
///
82
/// The label is `"{symbol}"` (`:name` appended when non-empty: `"{symbol} – {name}"`).
83
/// Returns `[]` on any parse failure — never panics.
84
19
pub fn parse_commodity_options(wire: &str) -> Vec<(String, String)> {
85
    use crate::eval::plist_field;
86
    use crate::render::{WireValue, parse_wire, reparse_list};
87
    use nomiscript::{Value, list_to_vec};
88

            
89
19
    let Ok(WireValue::Value(Value::String(ref s))) = parse_wire(wire) else {
90
2
        return vec![];
91
    };
92
17
    let Ok(list) = reparse_list(s) else {
93
        return vec![];
94
    };
95
17
    let Some(elements) = list_to_vec(&list) else {
96
        return vec![];
97
    };
98
17
    elements
99
17
        .iter()
100
17
        .filter_map(|e| {
101
17
            let id = plist_field(e, ":id")?;
102
17
            let symbol = plist_field(e, ":symbol")?;
103
17
            let label = match plist_field(e, ":name").filter(|n| !n.is_empty()) {
104
3
                Some(name) => format!("{symbol} \u{2013} {name}"),
105
14
                None => symbol,
106
            };
107
17
            Some((id, label))
108
17
        })
109
17
        .collect()
110
19
}