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

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

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

            
77
52
pub(super) fn parse_get_account_for_manage_id(id_arg: Option<String>) -> wasmtime::Result<Uuid> {
78
52
    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
51
    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
52
}
87

            
88
25
pub(super) async fn run_get_account_commodities(
89
25
    caller: &mut Caller<'_, SessionData>,
90
25
    user_id: Uuid,
91
25
    id_arg: Option<String>,
92
25
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
93
25
    let account_id = parse_account_commodities_id(id_arg)?;
94
25
    let result = GetAccountCommodities::new()
95
25
        .user_id(user_id)
96
25
        .account_id(account_id)
97
25
        .run()
98
25
        .await;
99
25
    let items = match result {
100
25
        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
25
    let mut anyrefs: Vec<Rooted<AnyRef>> = Vec::with_capacity(items.len());
114
25
    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
25
    alloc_pair_chain(caller, anyrefs).await
127
25
}
128

            
129
27
pub(super) fn parse_account_commodities_id(id_arg: Option<String>) -> wasmtime::Result<Uuid> {
130
27
    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
26
    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
27
}
139

            
140
576
pub(super) fn list_account_entries(
141
576
    name: &str,
142
576
    result: Result<Option<CmdResult>, CmdError>,
143
576
) -> wasmtime::Result<Vec<AccountEntry>> {
144
576
    match result {
145
576
        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => Ok(entities
146
576
            .into_iter()
147
576
            .filter_map(|(entity, tags)| match entity {
148
426
                FinanceEntity::Account(a) => Some((
149
426
                    a.id.to_string(),
150
426
                    tag_value_str(&tags, "name"),
151
426
                    a.parent.map(|u| u.to_string()),
152
426
                    tag_value_str(&tags, "type"),
153
                )),
154
                _ => None,
155
426
            })
156
576
            .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
576
}
164

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

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

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