Skip to main content

rustledger_wasm/
api.rs

1//! Public WASM API functions.
2//!
3//! These functions are exposed to JavaScript via wasm-bindgen.
4
5use wasm_bindgen::prelude::*;
6
7use rustledger_core::Directive;
8use rustledger_parser::parse as parse_beancount;
9
10use crate::convert::{directive_to_json, value_to_cell};
11use crate::helpers::{extract_options, load_and_interpolate, run_validation, to_js};
12#[cfg(feature = "completions")]
13use crate::types::{CompletionJson, CompletionResultJson};
14use crate::types::{
15    Error, FormatResult, Ledger, PadResult, ParseResult, QueryResult, ValidationResult,
16};
17#[cfg(feature = "plugins")]
18use crate::types::{PluginInfo, PluginResult};
19use crate::utils::LineLookup;
20
21/// Parse a Beancount source string.
22///
23/// Returns a `ParseResult` with the parsed ledger and any errors.
24#[wasm_bindgen]
25pub fn parse(source: &str) -> Result<JsValue, JsError> {
26    let result = parse_beancount(source);
27    let lookup = LineLookup::new(source);
28
29    let errors: Vec<Error> = result
30        .errors
31        .iter()
32        .map(|e| Error::with_line(e.to_string(), lookup.byte_to_line(e.span().0)))
33        .collect();
34
35    // Extract options from parsed result
36    let options = extract_options(&result.options);
37
38    let ledger = Some(Ledger {
39        directives: result
40            .directives
41            .iter()
42            .map(|spanned| directive_to_json(&spanned.value))
43            .collect(),
44        options,
45    });
46
47    let parse_result = ParseResult { ledger, errors };
48    to_js(&parse_result)
49}
50
51/// Validate a Beancount source string.
52///
53/// Parses, interpolates, and validates in one step.
54/// Returns a `ValidationResult` indicating whether the ledger is valid.
55#[wasm_bindgen(js_name = "validateSource")]
56pub fn validate_source(source: &str) -> Result<JsValue, JsError> {
57    let load = load_and_interpolate(source);
58    let validation_errors = run_validation(&load);
59    let mut errors = load.errors;
60    errors.extend(validation_errors);
61
62    let result = ValidationResult {
63        valid: errors.is_empty(),
64        errors,
65    };
66    to_js(&result)
67}
68
69/// Run a BQL query on a Beancount source string.
70///
71/// Parses the source, interpolates, then executes the query.
72/// Returns a `QueryResult` with columns, rows, and any errors.
73#[wasm_bindgen]
74pub fn query(source: &str, query_str: &str) -> Result<JsValue, JsError> {
75    use rustledger_query::{Executor, parse as parse_query};
76
77    let load = load_and_interpolate(source);
78
79    // Return early if there were parse/interpolation errors
80    if !load.errors.is_empty() {
81        let result = QueryResult {
82            columns: Vec::new(),
83            rows: Vec::new(),
84            errors: load.errors,
85        };
86        return to_js(&result);
87    }
88
89    // Parse the query
90    let query = match parse_query(query_str) {
91        Ok(q) => q,
92        Err(e) => {
93            let result = QueryResult {
94                columns: Vec::new(),
95                rows: Vec::new(),
96                errors: vec![Error::new(e.to_string())],
97            };
98            return to_js(&result);
99        }
100    };
101
102    let mut executor = Executor::new(&load.directives);
103    match executor.execute(&query) {
104        Ok(result) => {
105            let rows: Vec<Vec<_>> = result
106                .rows
107                .iter()
108                .map(|row| row.iter().map(value_to_cell).collect())
109                .collect();
110
111            let query_result = QueryResult {
112                columns: result.columns,
113                rows,
114                errors: Vec::new(),
115            };
116            to_js(&query_result)
117        }
118        Err(e) => {
119            let result = QueryResult {
120                columns: Vec::new(),
121                rows: Vec::new(),
122                errors: vec![Error::new(format!("Query execution error: {e}"))],
123            };
124            to_js(&result)
125        }
126    }
127}
128
129/// Get version information.
130///
131/// Returns the version string of the rustledger-wasm package.
132#[wasm_bindgen]
133pub fn version() -> String {
134    env!("CARGO_PKG_VERSION").to_string()
135}
136
137/// Format a Beancount source string.
138///
139/// Parses and reformats with consistent alignment.
140/// Returns a `FormatResult` with the formatted source or errors.
141#[wasm_bindgen]
142pub fn format(source: &str) -> Result<JsValue, JsError> {
143    use rustledger_core::{FormatConfig, format_directive};
144
145    let parse_result = parse_beancount(source);
146    let lookup = LineLookup::new(source);
147
148    if !parse_result.errors.is_empty() {
149        let result = FormatResult {
150            formatted: None,
151            errors: parse_result
152                .errors
153                .iter()
154                .map(|e| Error::with_line(e.to_string(), lookup.byte_to_line(e.span().0)))
155                .collect(),
156        };
157        return to_js(&result);
158    }
159
160    let config = FormatConfig::default();
161    let mut formatted = String::new();
162
163    for spanned in &parse_result.directives {
164        formatted.push_str(&format_directive(&spanned.value, &config));
165        formatted.push('\n');
166    }
167
168    let result = FormatResult {
169        formatted: Some(formatted),
170        errors: Vec::new(),
171    };
172    to_js(&result)
173}
174
175/// Process pad directives and expand them.
176///
177/// Returns directives with pad-generated transactions included.
178#[wasm_bindgen(js_name = "expandPads")]
179pub fn expand_pads(source: &str) -> Result<JsValue, JsError> {
180    use rustledger_booking::process_pads;
181
182    let load = load_and_interpolate(source);
183
184    // Return early if there were parse/interpolation errors
185    if !load.errors.is_empty() {
186        let result = PadResult {
187            directives: Vec::new(),
188            padding_transactions: Vec::new(),
189            errors: load.errors,
190        };
191        return to_js(&result);
192    }
193
194    // Process pads
195    let pad_result = process_pads(&load.directives);
196
197    let result = PadResult {
198        directives: pad_result
199            .directives
200            .iter()
201            .map(directive_to_json)
202            .collect(),
203        padding_transactions: pad_result
204            .padding_transactions
205            .iter()
206            .map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
207            .collect(),
208        errors: pad_result
209            .errors
210            .iter()
211            .map(|e| Error::new(e.message.clone()))
212            .collect(),
213    };
214    to_js(&result)
215}
216
217/// Run a native plugin on the source.
218///
219/// Available plugins can be listed with `listPlugins()`.
220#[cfg(feature = "plugins")]
221#[wasm_bindgen(js_name = "runPlugin")]
222pub fn run_plugin(source: &str, plugin_name: &str) -> Result<JsValue, JsError> {
223    use rustledger_plugin::{
224        NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
225        wrappers_to_directives,
226    };
227
228    let load = load_and_interpolate(source);
229
230    // Return early if there were parse/interpolation errors
231    if !load.errors.is_empty() {
232        let result = PluginResult {
233            directives: Vec::new(),
234            errors: load.errors,
235        };
236        return to_js(&result);
237    }
238
239    // Find and run the plugin
240    let registry = NativePluginRegistry::new();
241    let Some(plugin) = registry.find(plugin_name) else {
242        let result = PluginResult {
243            directives: Vec::new(),
244            errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
245        };
246        return to_js(&result);
247    };
248
249    // Convert directives to plugin format and run
250    let wrappers = directives_to_wrappers(&load.directives);
251    let input = PluginInput {
252        directives: wrappers,
253        options: PluginOptions::default(),
254        config: None,
255    };
256
257    let output = plugin.process(input);
258
259    // Convert back
260    let output_directives = match wrappers_to_directives(&output.directives) {
261        Ok(dirs) => dirs,
262        Err(e) => {
263            let result = PluginResult {
264                directives: Vec::new(),
265                errors: vec![Error::new(format!("Conversion error: {e}"))],
266            };
267            return to_js(&result);
268        }
269    };
270
271    let result = PluginResult {
272        directives: output_directives.iter().map(directive_to_json).collect(),
273        errors: output
274            .errors
275            .iter()
276            .map(|e| match e.severity {
277                rustledger_plugin::PluginErrorSeverity::Warning => {
278                    Error::warning(e.message.clone())
279                }
280                rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
281            })
282            .collect(),
283    };
284    to_js(&result)
285}
286
287/// List available native plugins.
288///
289/// Returns an array of `PluginInfo` objects with name and description.
290#[cfg(feature = "plugins")]
291#[wasm_bindgen(js_name = "listPlugins")]
292pub fn list_plugins() -> Result<JsValue, JsError> {
293    use rustledger_plugin::NativePluginRegistry;
294
295    let registry = NativePluginRegistry::new();
296    let plugins: Vec<PluginInfo> = registry
297        .list()
298        .iter()
299        .map(|p| PluginInfo {
300            name: p.name().to_string(),
301            description: p.description().to_string(),
302        })
303        .collect();
304
305    to_js(&plugins)
306}
307
308/// Calculate account balances.
309///
310/// Shorthand for `query(source, "BALANCES")`.
311#[wasm_bindgen]
312pub fn balances(source: &str) -> Result<JsValue, JsError> {
313    query(source, "BALANCES")
314}
315
316/// Get BQL query completions at cursor position.
317///
318/// Returns context-aware completions for the BQL query language.
319#[cfg(feature = "completions")]
320#[wasm_bindgen(js_name = "bqlCompletions")]
321pub fn bql_completions(partial_query: &str, cursor_pos: usize) -> Result<JsValue, JsError> {
322    use rustledger_query::completions;
323
324    let result = completions::complete(partial_query, cursor_pos);
325
326    let json_result = CompletionResultJson {
327        completions: result
328            .completions
329            .into_iter()
330            .map(|c| CompletionJson {
331                text: c.text,
332                category: c.category.as_str().to_string(),
333                description: c.description,
334            })
335            .collect(),
336        context: format!("{:?}", result.context),
337    };
338
339    to_js(&json_result)
340}