1
use finance::tag::Tag;
2
use scripting::runtime::{alloc_entity_via_export, alloc_pair_chain, alloc_string_ref};
3
#[cfg(test)]
4
use server::command::CommodityInfo;
5
use server::command::account::{GetAccount, GetAccountCommodities, GetAccountForManage};
6
use server::command::{CmdError, CmdResult, FinanceEntity};
7
use uuid::Uuid;
8
use wasmtime::{AnyRef, Caller, Rooted, StructRef, Val};
9

            
10
use crate::session::SessionData;
11

            
12
pub(super) type AccountEntry = (String, Option<String>, Option<String>, Option<String>);
13

            
14
390
pub(super) async fn run_get_account(
15
390
    caller: &mut Caller<'_, SessionData>,
16
390
    user_id: Uuid,
17
390
    key_arg: Option<String>,
18
390
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
19
390
    let key = validate_lookup_key("get-account", key_arg)?;
20
390
    let mut runner = GetAccount::new().user_id(user_id);
21
390
    let result = match Uuid::parse_str(&key) {
22
234
        Ok(id) => runner.account_id(id).run().await,
23
        Err(_) => {
24
156
            runner = runner.account_name(key);
25
156
            runner.run().await
26
        }
27
    };
28
390
    let entries = list_account_entries("get-account", result)?;
29
390
    match entries.into_iter().next() {
30
312
        Some((id, name, parent, type_val)) => Ok(Some(
31
312
            alloc_account_entity(
32
312
                caller,
33
312
                &id,
34
312
                name.as_deref(),
35
312
                parent.as_deref(),
36
312
                type_val.as_deref(),
37
312
            )
38
312
            .await?,
39
        )),
40
78
        None => Ok(None),
41
    }
42
390
}
43

            
44
392
pub(super) fn validate_lookup_key(name: &str, key_arg: Option<String>) -> wasmtime::Result<String> {
45
392
    key_arg
46
392
        .filter(|s| !s.is_empty())
47
392
        .ok_or_else(|| wasmtime::Error::msg(format!("{name}: missing or empty lookup key")))
48
392
}
49

            
50
52
pub(super) async fn run_get_account_for_manage(
51
52
    caller: &mut Caller<'_, SessionData>,
52
52
    user_id: Uuid,
53
52
    id_arg: Option<String>,
54
52
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
55
52
    let account_id = parse_get_account_for_manage_id(id_arg)?;
56
52
    let result = GetAccountForManage::new()
57
52
        .user_id(user_id)
58
52
        .account_id(account_id)
59
52
        .run()
60
52
        .await;
61
52
    let entries = list_account_entries("get-account-for-manage", result)?;
62
52
    match entries.into_iter().next() {
63
26
        Some((id, name, parent, type_val)) => Ok(Some(
64
26
            alloc_account_entity(
65
26
                caller,
66
26
                &id,
67
26
                name.as_deref(),
68
26
                parent.as_deref(),
69
26
                type_val.as_deref(),
70
26
            )
71
26
            .await?,
72
        )),
73
26
        None => Ok(None),
74
    }
75
52
}
76

            
77
54
pub(super) fn parse_get_account_for_manage_id(id_arg: Option<String>) -> wasmtime::Result<Uuid> {
78
54
    let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
79
1
        wasmtime::Error::msg("get-account-for-manage: missing or empty :account-id arg")
80
1
    })?;
81
53
    Uuid::parse_str(&raw).map_err(|err| {
82
1
        wasmtime::Error::msg(format!(
83
            "get-account-for-manage: invalid uuid '{raw}': {err}"
84
        ))
85
1
    })
86
54
}
87

            
88
26
pub(super) async fn run_get_account_commodities(
89
26
    caller: &mut Caller<'_, SessionData>,
90
26
    user_id: Uuid,
91
26
    id_arg: Option<String>,
92
26
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
93
26
    let account_id = parse_account_commodities_id(id_arg)?;
94
26
    let result = GetAccountCommodities::new()
95
26
        .user_id(user_id)
96
26
        .account_id(account_id)
97
26
        .run()
98
26
        .await;
99
26
    let items = match result {
100
26
        Ok(Some(CmdResult::CommodityInfoList(items))) => items,
101
        Ok(Some(other)) => {
102
            return Err(wasmtime::Error::msg(format!(
103
                "get-account-commodities: expected CommodityInfoList, got {other:?}"
104
            )));
105
        }
106
        Ok(None) => Vec::new(),
107
        Err(err) => {
108
            return Err(wasmtime::Error::msg(format!(
109
                "get-account-commodities: {err}"
110
            )));
111
        }
112
    };
113
26
    let mut anyrefs: Vec<Rooted<AnyRef>> = Vec::with_capacity(items.len());
114
26
    for info in items {
115
        let id_ref = alloc_string_ref(caller, info.commodity_id.to_string().as_bytes())?;
116
        let symbol_ref = alloc_string_ref(caller, info.symbol.as_bytes())?;
117
        let name_ref = alloc_string_ref(caller, info.name.as_bytes())?;
118
        let args = [
119
            Val::AnyRef(Some(id_ref.to_anyref())),
120
            Val::AnyRef(Some(symbol_ref.to_anyref())),
121
            Val::AnyRef(Some(name_ref.to_anyref())),
122
        ];
123
        let entity_ref = alloc_entity_via_export(caller, "alloc_commodity_entity", &args).await?;
124
        anyrefs.push(entity_ref.to_anyref());
125
    }
126
26
    alloc_pair_chain(caller, anyrefs).await
127
26
}
128

            
129
28
pub(super) fn parse_account_commodities_id(id_arg: Option<String>) -> wasmtime::Result<Uuid> {
130
28
    let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
131
1
        wasmtime::Error::msg("get-account-commodities: missing or empty :account-id arg")
132
1
    })?;
133
27
    Uuid::parse_str(&raw).map_err(|err| {
134
1
        wasmtime::Error::msg(format!(
135
            "get-account-commodities: invalid uuid '{raw}': {err}"
136
        ))
137
1
    })
138
28
}
139

            
140
599
pub(super) fn list_account_entries(
141
599
    name: &str,
142
599
    result: Result<Option<CmdResult>, CmdError>,
143
599
) -> wasmtime::Result<Vec<AccountEntry>> {
144
599
    match result {
145
599
        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => Ok(entities
146
599
            .into_iter()
147
599
            .filter_map(|(entity, tags)| match entity {
148
443
                FinanceEntity::Account(a) => Some((
149
443
                    a.id.to_string(),
150
443
                    tag_value_str(&tags, "name"),
151
443
                    a.parent.map(|u| u.to_string()),
152
443
                    tag_value_str(&tags, "type"),
153
                )),
154
                _ => None,
155
443
            })
156
599
            .collect()),
157
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
158
            "{name}: expected TaggedEntities, got {other:?}"
159
        ))),
160
        Ok(None) => Ok(Vec::new()),
161
        Err(err) => Err(wasmtime::Error::msg(format!("{name}: {err}"))),
162
    }
163
599
}
164

            
165
886
pub(super) fn tag_value_str(
166
886
    tags: &std::collections::HashMap<String, FinanceEntity>,
167
886
    key: &str,
168
886
) -> Option<String> {
169
886
    tags.get(key).and_then(|t| match t {
170
443
        FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.clone()),
171
        _ => None,
172
443
    })
173
886
}
174

            
175
442
pub(super) async fn alloc_account_entity(
176
442
    caller: &mut Caller<'_, SessionData>,
177
442
    id: &str,
178
442
    name: Option<&str>,
179
442
    parent: Option<&str>,
180
442
    type_val: Option<&str>,
181
442
) -> wasmtime::Result<Rooted<StructRef>> {
182
442
    let id_ref = alloc_string_ref(caller, id.as_bytes())?;
183
442
    let name_ref = name
184
442
        .map(|s| alloc_string_ref(caller, s.as_bytes()))
185
442
        .transpose()?;
186
442
    let parent_ref = parent
187
442
        .map(|s| alloc_string_ref(caller, s.as_bytes()))
188
442
        .transpose()?;
189
442
    let type_ref = type_val
190
442
        .map(|s| alloc_string_ref(caller, s.as_bytes()))
191
442
        .transpose()?;
192
442
    let args = [
193
442
        Val::AnyRef(Some(id_ref.to_anyref())),
194
442
        Val::AnyRef(name_ref.map(|r| r.to_anyref())),
195
442
        Val::AnyRef(parent_ref.map(|r| r.to_anyref())),
196
442
        Val::AnyRef(type_ref.map(|r| r.to_anyref())),
197
    ];
198
442
    alloc_entity_via_export(caller, "alloc_account", &args).await
199
442
}
200

            
201
156
pub(super) async fn alloc_account_chain(
202
156
    caller: &mut Caller<'_, SessionData>,
203
156
    entries: Vec<AccountEntry>,
204
156
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
205
156
    let mut anyrefs: Vec<Rooted<AnyRef>> = Vec::with_capacity(entries.len());
206
156
    for (id, name, parent, type_val) in entries {
207
104
        let entity_ref = alloc_account_entity(
208
104
            caller,
209
104
            &id,
210
104
            name.as_deref(),
211
104
            parent.as_deref(),
212
104
            type_val.as_deref(),
213
104
        )
214
104
        .await?;
215
104
        anyrefs.push(entity_ref.to_anyref());
216
    }
217
156
    alloc_pair_chain(caller, anyrefs).await
218
156
}
219

            
220
#[cfg(test)]
221
3
pub(super) fn format_manage_tree(
222
3
    entities: &[(
223
3
        FinanceEntity,
224
3
        std::collections::HashMap<String, FinanceEntity>,
225
3
    )],
226
3
) -> String {
227
3
    let mut out = String::from("(:accounts-tree (");
228
3
    for (idx, (entity, tags)) in entities.iter().enumerate() {
229
2
        if idx > 0 {
230
            out.push(' ');
231
2
        }
232
2
        match entity {
233
2
            FinanceEntity::Account(account) => {
234
2
                let parent = match account.parent {
235
1
                    Some(p) => format!("\"{p}\""),
236
1
                    None => "nil".to_string(),
237
                };
238
2
                out.push_str(&format!("(:id \"{}\" :parent-id {}", account.id, parent));
239
2
                if let Some(name) = tags.get("name").and_then(|t| match t {
240
1
                    FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.as_str()),
241
                    _ => None,
242
1
                }) {
243
1
                    out.push_str(&format!(" :name {}", super::quote_string(name)));
244
1
                }
245
2
                out.push(')');
246
            }
247
            other => {
248
                out.push_str(&format!("(:error \"unexpected entity {other:?}\")"));
249
            }
250
        }
251
    }
252
3
    out.push_str("))");
253
3
    out
254
3
}
255

            
256
#[cfg(test)]
257
4
pub(super) fn format_tagged_entities(
258
4
    entities: &[(
259
4
        FinanceEntity,
260
4
        std::collections::HashMap<String, FinanceEntity>,
261
4
    )],
262
4
) -> String {
263
4
    let mut out = String::from("(:accounts (");
264
4
    for (idx, (entity, tags)) in entities.iter().enumerate() {
265
3
        if idx > 0 {
266
            out.push(' ');
267
3
        }
268
3
        let id = match entity {
269
3
            FinanceEntity::Account(a) => a.id,
270
            other => {
271
                out.push_str(&format!("(:error \"unexpected entity {other:?}\")"));
272
                continue;
273
            }
274
        };
275
3
        let name = tags.get("name").and_then(|t| match t {
276
2
            FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.as_str()),
277
            _ => None,
278
2
        });
279
3
        out.push_str(&format!("(:id \"{id}\""));
280
3
        if let Some(name) = name {
281
2
            out.push_str(&format!(" :name {}", super::quote_string(name)));
282
2
        }
283
3
        out.push(')');
284
    }
285
4
    out.push_str("))");
286
4
    out
287
4
}
288

            
289
#[cfg(test)]
290
2
pub(super) fn format_commodity_info_list(items: &[CommodityInfo]) -> String {
291
2
    let mut out = String::from("(:account-commodities (");
292
2
    for (idx, info) in items.iter().enumerate() {
293
2
        if idx > 0 {
294
1
            out.push(' ');
295
1
        }
296
2
        out.push_str(&format!(
297
2
            "(:commodity-id \"{}\" :symbol {} :name {})",
298
2
            info.commodity_id,
299
2
            super::quote_string(&info.symbol),
300
2
            super::quote_string(&info.name),
301
2
        ));
302
    }
303
2
    out.push_str("))");
304
2
    out
305
2
}