1use 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#[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 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#[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#[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 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 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#[wasm_bindgen]
133pub fn version() -> String {
134 env!("CARGO_PKG_VERSION").to_string()
135}
136
137#[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#[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 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 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#[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 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 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 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 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#[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#[wasm_bindgen]
312pub fn balances(source: &str) -> Result<JsValue, JsError> {
313 query(source, "BALANCES")
314}
315
316#[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}