1use std::collections::HashMap;
6use std::path::Path;
7use wasm_bindgen::prelude::*;
8
9use rustledger_core::Directive;
10use rustledger_loader::{FileSystem, LoadError, LoadResult};
11use rustledger_parser::parse as parse_beancount;
12
13use crate::convert::{directive_to_json, value_to_cell};
14use crate::helpers::{extract_options, load_and_book, run_validation, to_js};
15#[cfg(feature = "completions")]
16use crate::types::{CompletionJson, CompletionResultJson};
17use crate::types::{
18 Error, FormatResult, Ledger, PadResult, ParseResult, QueryResult, Severity, ValidationResult,
19};
20#[cfg(feature = "plugins")]
21use crate::types::{PluginInfo, PluginResult};
22use crate::utils::LineLookup;
23
24fn load_errors_to_errors(load_result: &LoadResult) -> Vec<Error> {
28 let mut errors = Vec::new();
29
30 for load_error in &load_result.errors {
31 match load_error {
32 LoadError::ParseErrors {
33 path,
34 errors: parse_errors,
35 } => {
36 for parse_error in parse_errors {
38 let span = parse_error.span();
39 let line = load_result
41 .source_map
42 .get_by_path(path)
43 .map(|file| file.line_col(span.0).0 as u32);
44
45 let msg = format!("{}: {}", path.display(), parse_error);
46 if let Some(line_num) = line {
47 errors.push(Error::with_line(msg, line_num));
48 } else {
49 errors.push(Error::new(msg));
50 }
51 }
52 }
53 other => {
54 errors.push(Error::new(other.to_string()));
56 }
57 }
58 }
59
60 errors
61}
62
63#[wasm_bindgen]
67pub fn parse(source: &str) -> Result<JsValue, JsError> {
68 let result = parse_beancount(source);
69 let lookup = LineLookup::new(source);
70
71 let errors: Vec<Error> = result
72 .errors
73 .iter()
74 .map(|e| Error::with_line(e.to_string(), lookup.byte_to_line(e.span().0)))
75 .collect();
76
77 let options = extract_options(&result.options);
79
80 let ledger = Some(Ledger {
81 directives: result
82 .directives
83 .iter()
84 .map(|spanned| directive_to_json(&spanned.value))
85 .collect(),
86 options,
87 });
88
89 let parse_result = ParseResult { ledger, errors };
90 to_js(&parse_result)
91}
92
93#[wasm_bindgen(js_name = "validateSource")]
98pub fn validate_source(source: &str) -> Result<JsValue, JsError> {
99 let load = load_and_book(source);
100 let validation_errors = run_validation(&load);
101 let mut errors = load.errors;
102 errors.extend(validation_errors);
103
104 let result = ValidationResult {
105 valid: errors.is_empty(),
106 errors,
107 };
108 to_js(&result)
109}
110
111#[wasm_bindgen]
116pub fn query(source: &str, query_str: &str) -> Result<JsValue, JsError> {
117 use rustledger_query::{Executor, parse as parse_query};
118
119 let load = load_and_book(source);
120
121 if !load.errors.is_empty() {
123 let result = QueryResult {
124 columns: Vec::new(),
125 rows: Vec::new(),
126 errors: load.errors,
127 };
128 return to_js(&result);
129 }
130
131 let query = match parse_query(query_str) {
133 Ok(q) => q,
134 Err(e) => {
135 let result = QueryResult {
136 columns: Vec::new(),
137 rows: Vec::new(),
138 errors: vec![Error::new(e.to_string())],
139 };
140 return to_js(&result);
141 }
142 };
143
144 let mut executor = Executor::new(&load.directives);
145 match executor.execute(&query) {
146 Ok(result) => {
147 let rows: Vec<Vec<_>> = result
148 .rows
149 .iter()
150 .map(|row| row.iter().map(value_to_cell).collect())
151 .collect();
152
153 let query_result = QueryResult {
154 columns: result.columns,
155 rows,
156 errors: Vec::new(),
157 };
158 to_js(&query_result)
159 }
160 Err(e) => {
161 let result = QueryResult {
162 columns: Vec::new(),
163 rows: Vec::new(),
164 errors: vec![Error::new(format!("Query execution error: {e}"))],
165 };
166 to_js(&result)
167 }
168 }
169}
170
171#[wasm_bindgen]
175pub fn version() -> String {
176 env!("CARGO_PKG_VERSION").to_string()
177}
178
179#[wasm_bindgen]
184pub fn format(source: &str) -> Result<JsValue, JsError> {
185 use rustledger_core::{FormatConfig, format_directive};
186
187 let parse_result = parse_beancount(source);
188 let lookup = LineLookup::new(source);
189
190 if !parse_result.errors.is_empty() {
191 let result = FormatResult {
192 formatted: None,
193 errors: parse_result
194 .errors
195 .iter()
196 .map(|e| Error::with_line(e.to_string(), lookup.byte_to_line(e.span().0)))
197 .collect(),
198 };
199 return to_js(&result);
200 }
201
202 let config = FormatConfig::default();
203 let mut formatted = String::new();
204
205 for spanned in &parse_result.directives {
206 formatted.push_str(&format_directive(&spanned.value, &config));
207 formatted.push('\n');
208 }
209
210 let result = FormatResult {
211 formatted: Some(formatted),
212 errors: Vec::new(),
213 };
214 to_js(&result)
215}
216
217#[wasm_bindgen(js_name = "expandPads")]
221pub fn expand_pads(source: &str) -> Result<JsValue, JsError> {
222 use rustledger_booking::process_pads;
223
224 let load = load_and_book(source);
225
226 if !load.errors.is_empty() {
228 let result = PadResult {
229 directives: Vec::new(),
230 padding_transactions: Vec::new(),
231 errors: load.errors,
232 };
233 return to_js(&result);
234 }
235
236 let pad_result = process_pads(&load.directives);
238
239 let result = PadResult {
240 directives: pad_result
241 .directives
242 .iter()
243 .map(directive_to_json)
244 .collect(),
245 padding_transactions: pad_result
246 .padding_transactions
247 .iter()
248 .map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
249 .collect(),
250 errors: pad_result
251 .errors
252 .iter()
253 .map(|e| Error::new(e.message.clone()))
254 .collect(),
255 };
256 to_js(&result)
257}
258
259#[cfg(feature = "plugins")]
263#[wasm_bindgen(js_name = "runPlugin")]
264pub fn run_plugin(source: &str, plugin_name: &str) -> Result<JsValue, JsError> {
265 use rustledger_plugin::{
266 NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
267 wrappers_to_directives,
268 };
269
270 let load = load_and_book(source);
271
272 if !load.errors.is_empty() {
274 let result = PluginResult {
275 directives: Vec::new(),
276 errors: load.errors,
277 };
278 return to_js(&result);
279 }
280
281 let registry = NativePluginRegistry::new();
283 let Some(plugin) = registry.find(plugin_name) else {
284 let result = PluginResult {
285 directives: Vec::new(),
286 errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
287 };
288 return to_js(&result);
289 };
290
291 let wrappers = directives_to_wrappers(&load.directives);
293 let input = PluginInput {
294 directives: wrappers,
295 options: PluginOptions::default(),
296 config: None,
297 };
298
299 let output = plugin.process(input);
300
301 let output_directives = match wrappers_to_directives(&output.directives) {
303 Ok(dirs) => dirs,
304 Err(e) => {
305 let result = PluginResult {
306 directives: Vec::new(),
307 errors: vec![Error::new(format!("Conversion error: {e}"))],
308 };
309 return to_js(&result);
310 }
311 };
312
313 let result = PluginResult {
314 directives: output_directives.iter().map(directive_to_json).collect(),
315 errors: output
316 .errors
317 .iter()
318 .map(|e| match e.severity {
319 rustledger_plugin::PluginErrorSeverity::Warning => {
320 Error::warning(e.message.clone())
321 }
322 rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
323 })
324 .collect(),
325 };
326 to_js(&result)
327}
328
329#[cfg(feature = "plugins")]
333#[wasm_bindgen(js_name = "listPlugins")]
334pub fn list_plugins() -> Result<JsValue, JsError> {
335 use rustledger_plugin::NativePluginRegistry;
336
337 let registry = NativePluginRegistry::new();
338 let plugins: Vec<PluginInfo> = registry
339 .list()
340 .iter()
341 .map(|p| PluginInfo {
342 name: p.name().to_string(),
343 description: p.description().to_string(),
344 })
345 .collect();
346
347 to_js(&plugins)
348}
349
350#[wasm_bindgen]
354pub fn balances(source: &str) -> Result<JsValue, JsError> {
355 query(source, "BALANCES")
356}
357
358#[cfg(feature = "completions")]
362#[wasm_bindgen(js_name = "bqlCompletions")]
363pub fn bql_completions(partial_query: &str, cursor_pos: usize) -> Result<JsValue, JsError> {
364 use rustledger_query::completions;
365
366 let result = completions::complete(partial_query, cursor_pos);
367
368 let json_result = CompletionResultJson {
369 completions: result
370 .completions
371 .into_iter()
372 .map(|c| CompletionJson {
373 text: c.text,
374 category: c.category.as_str().to_string(),
375 description: c.description,
376 })
377 .collect(),
378 context: format!("{:?}", result.context),
379 };
380
381 to_js(&json_result)
382}
383
384#[wasm_bindgen(js_name = "parseMultiFile")]
417pub fn parse_multi_file(files: JsValue, entry_point: &str) -> Result<JsValue, JsError> {
418 use rustledger_booking::interpolate;
419 use rustledger_loader::{Loader, VirtualFileSystem};
420
421 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
423 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
424
425 if file_map.is_empty() {
426 return Err(JsError::new("Files map cannot be empty"));
427 }
428
429 let vfs = VirtualFileSystem::from_files(file_map);
431
432 if !vfs.exists(Path::new(entry_point)) {
434 return Err(JsError::new(&format!(
435 "Entry point '{entry_point}' not found in files map"
436 )));
437 }
438
439 let mut loader = Loader::new().with_filesystem(Box::new(vfs));
441
442 let load_result = match loader.load(Path::new(entry_point)) {
444 Ok(result) => result,
445 Err(e) => {
446 let result = ParseResult {
447 ledger: None,
448 errors: vec![Error::new(format!("Load error: {e}"))],
449 };
450 return to_js(&result);
451 }
452 };
453
454 let mut errors = load_errors_to_errors(&load_result);
456
457 let options = crate::types::LedgerOptions {
459 title: load_result.options.title.clone(),
460 operating_currencies: load_result.options.operating_currency.clone(),
461 };
462
463 let mut directives: Vec<Directive> = load_result
465 .directives
466 .into_iter()
467 .map(|s| s.value)
468 .collect();
469
470 if errors.is_empty() {
472 for directive in &mut directives {
473 if let Directive::Transaction(txn) = directive {
474 match interpolate(txn) {
475 Ok(result) => {
476 *txn = result.transaction;
477 }
478 Err(e) => {
479 errors.push(Error::new(e.to_string()));
480 }
481 }
482 }
483 }
484 }
485
486 let ledger = Some(Ledger {
487 directives: directives.iter().map(directive_to_json).collect(),
488 options,
489 });
490
491 let result = ParseResult { ledger, errors };
492 to_js(&result)
493}
494
495#[wasm_bindgen(js_name = "validateMultiFile")]
500pub fn validate_multi_file(files: JsValue, entry_point: &str) -> Result<JsValue, JsError> {
501 use rustledger_loader::{LoadOptions, Loader, VirtualFileSystem, process};
502
503 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
505 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
506
507 if file_map.is_empty() {
508 return Err(JsError::new("Files map cannot be empty"));
509 }
510
511 let vfs = VirtualFileSystem::from_files(file_map);
513
514 if !vfs.exists(Path::new(entry_point)) {
516 return Err(JsError::new(&format!(
517 "Entry point '{entry_point}' not found in files map"
518 )));
519 }
520
521 let mut loader = Loader::new().with_filesystem(Box::new(vfs));
523
524 let load_result = match loader.load(Path::new(entry_point)) {
526 Ok(result) => result,
527 Err(e) => {
528 let result = ValidationResult {
529 valid: false,
530 errors: vec![Error::new(format!("Load error: {e}"))],
531 };
532 return to_js(&result);
533 }
534 };
535
536 let parse_errors = load_errors_to_errors(&load_result);
538 if !parse_errors.is_empty() {
539 let result = ValidationResult {
540 valid: false,
541 errors: parse_errors,
542 };
543 return to_js(&result);
544 }
545
546 let options = LoadOptions {
548 validate: true,
549 ..Default::default()
550 };
551
552 let ledger = match process(load_result, &options) {
553 Ok(ledger) => ledger,
554 Err(e) => {
555 let result = ValidationResult {
556 valid: false,
557 errors: vec![Error::new(format!("Processing error: {e}"))],
558 };
559 return to_js(&result);
560 }
561 };
562
563 let errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
564
565 let result = ValidationResult {
566 valid: errors.is_empty(),
567 errors,
568 };
569 to_js(&result)
570}
571
572#[wasm_bindgen(js_name = "queryMultiFile")]
579pub fn query_multi_file(
580 files: JsValue,
581 entry_point: &str,
582 query_str: &str,
583) -> Result<JsValue, JsError> {
584 use rustledger_booking::expand_pads;
585 use rustledger_loader::{LoadOptions, Loader, VirtualFileSystem, process};
586 use rustledger_query::{Executor, parse as parse_query};
587
588 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
590 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
591
592 if file_map.is_empty() {
593 return Err(JsError::new("Files map cannot be empty"));
594 }
595
596 let vfs = VirtualFileSystem::from_files(file_map);
598
599 if !vfs.exists(Path::new(entry_point)) {
601 return Err(JsError::new(&format!(
602 "Entry point '{entry_point}' not found in files map"
603 )));
604 }
605
606 let mut loader = Loader::new().with_filesystem(Box::new(vfs));
608
609 let load_result = match loader.load(Path::new(entry_point)) {
611 Ok(result) => result,
612 Err(e) => {
613 let result = QueryResult {
614 columns: Vec::new(),
615 rows: Vec::new(),
616 errors: vec![Error::new(format!("Load error: {e}"))],
617 };
618 return to_js(&result);
619 }
620 };
621
622 let parse_errors = load_errors_to_errors(&load_result);
624 if !parse_errors.is_empty() {
625 let result = QueryResult {
626 columns: Vec::new(),
627 rows: Vec::new(),
628 errors: parse_errors,
629 };
630 return to_js(&result);
631 }
632
633 let options = LoadOptions {
635 validate: false,
636 ..Default::default()
637 };
638
639 let ledger = match process(load_result, &options) {
640 Ok(ledger) => ledger,
641 Err(e) => {
642 let result = QueryResult {
643 columns: Vec::new(),
644 rows: Vec::new(),
645 errors: vec![Error::new(format!("Processing error: {e}"))],
646 };
647 return to_js(&result);
648 }
649 };
650
651 let errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
653 let has_errors = errors.iter().any(|e| e.severity == Severity::Error);
654 if has_errors {
655 let result = QueryResult {
656 columns: Vec::new(),
657 rows: Vec::new(),
658 errors,
659 };
660 return to_js(&result);
661 }
662
663 let booked_directives: Vec<_> = ledger.directives.into_iter().map(|s| s.value).collect();
665 let directives = expand_pads(&booked_directives);
666
667 let query = match parse_query(query_str) {
669 Ok(q) => q,
670 Err(e) => {
671 let result = QueryResult {
672 columns: Vec::new(),
673 rows: Vec::new(),
674 errors: vec![Error::new(e.to_string())],
675 };
676 return to_js(&result);
677 }
678 };
679
680 let mut executor = Executor::new(&directives);
682 match executor.execute(&query) {
683 Ok(result) => {
684 let rows: Vec<Vec<_>> = result
685 .rows
686 .iter()
687 .map(|row| row.iter().map(value_to_cell).collect())
688 .collect();
689
690 let query_result = QueryResult {
691 columns: result.columns,
692 rows,
693 errors: Vec::new(),
694 };
695 to_js(&query_result)
696 }
697 Err(e) => {
698 let result = QueryResult {
699 columns: Vec::new(),
700 rows: Vec::new(),
701 errors: vec![Error::new(format!("Query execution error: {e}"))],
702 };
703 to_js(&result)
704 }
705 }
706}
707
708#[wasm_bindgen(js_name = "hashSources")]
721#[allow(clippy::needless_pass_by_value)] pub fn hash_sources(sources: Vec<String>) -> String {
723 let refs: Vec<&str> = sources.iter().map(String::as_str).collect();
724 crate::cache::hash_sources(&refs)
725}