1use std::collections::HashMap;
8use std::path::Path;
9use wasm_bindgen::prelude::*;
10
11use rustledger_core::Directive;
12use rustledger_parser::ParseResult as ParserResult;
13
14use crate::cache;
15use crate::convert::directive_to_json;
16use crate::editor;
17use crate::helpers::{load_and_book, run_validation, to_js};
18#[cfg(feature = "plugins")]
19use crate::types::PluginResult;
20use crate::types::{Error, FormatResult, LedgerOptions, PadResult, QueryResult};
21
22fn execute_query(
27 directives: &[Directive],
28 query_str: &str,
29 account_types: rustledger_core::AccountTypes,
30) -> Result<JsValue, JsError> {
31 use crate::convert::value_to_cell;
32 use rustledger_query::{Executor, parse as parse_query};
33
34 let query = match parse_query(query_str) {
35 Ok(q) => q,
36 Err(e) => {
37 let result = QueryResult {
38 columns: Vec::new(),
39 rows: Vec::new(),
40 errors: vec![Error::new(e.to_string())],
41 };
42 return to_js(&result);
43 }
44 };
45
46 let expanded = rustledger_booking::merge_with_padding(directives);
54 let mut executor = Executor::new(&expanded);
55 executor.set_account_types(account_types);
59 match executor.execute(&query) {
60 Ok(result) => {
61 let rows: Vec<Vec<_>> = result
62 .rows
63 .iter()
64 .map(|row| row.iter().map(value_to_cell).collect())
65 .collect();
66
67 let query_result = QueryResult {
68 columns: result.columns,
69 rows,
70 errors: Vec::new(),
71 };
72 to_js(&query_result)
73 }
74 Err(e) => {
75 let result = QueryResult {
76 columns: Vec::new(),
77 rows: Vec::new(),
78 errors: vec![Error::new(format!("Query execution error: {e}"))],
79 };
80 to_js(&result)
81 }
82 }
83}
84
85fn execute_expand_pads(directives: &[Directive]) -> Result<JsValue, JsError> {
86 use rustledger_booking::process_pads;
87
88 let pad_result = process_pads(directives);
89
90 let result = PadResult {
91 directives: directives.iter().map(directive_to_json).collect(),
95 padding_transactions: pad_result
96 .padding_transactions
97 .iter()
98 .map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
99 .collect(),
100 errors: pad_result
101 .errors
102 .iter()
103 .map(|e| Error::new(e.message.clone()))
104 .collect(),
105 };
106 to_js(&result)
107}
108
109#[cfg(feature = "plugins")]
110fn execute_plugin(directives: &[Directive], plugin_name: &str) -> Result<JsValue, JsError> {
111 use rustledger_plugin::{
112 NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
113 wrappers_to_directives,
114 };
115
116 let registry = NativePluginRegistry::global();
117 let Some(plugin) = registry.find_regular(plugin_name) else {
121 let result = PluginResult {
122 directives: Vec::new(),
123 errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
124 };
125 return to_js(&result);
126 };
127
128 let wrappers = directives_to_wrappers(directives);
129 let input = PluginInput {
130 directives: wrappers,
131 options: PluginOptions::default(),
132 config: None,
133 };
134
135 let input_dirs = input.directives.clone();
136 let output = plugin.process(input);
137 let materialized = crate::api::materialize_plugin_ops(&input_dirs, &output);
138
139 let output_directives = match wrappers_to_directives(&materialized) {
140 Ok(dirs) => dirs,
141 Err(e) => {
142 let result = PluginResult {
143 directives: Vec::new(),
144 errors: vec![Error::new(format!("Conversion error: {e}"))],
145 };
146 return to_js(&result);
147 }
148 };
149
150 let result = PluginResult {
151 directives: output_directives.iter().map(directive_to_json).collect(),
152 errors: output
153 .errors
154 .iter()
155 .map(|e| match e.severity {
156 rustledger_plugin::PluginErrorSeverity::Warning => {
157 Error::warning(e.message.clone())
158 }
159 rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
160 })
161 .collect(),
162 };
163 to_js(&result)
164}
165
166#[wasm_bindgen(skip_typescript)]
187pub struct ParsedLedger {
188 source: String,
190 parse_result: ParserResult,
192 directives: Vec<Directive>,
194 options: LedgerOptions,
196 parse_errors: Vec<Error>,
198 validation_errors: Vec<Error>,
200 editor_cache: editor::EditorCache,
202}
203
204#[wasm_bindgen]
205impl ParsedLedger {
206 #[wasm_bindgen(constructor)]
210 pub fn new(source: &str) -> Self {
211 let load = load_and_book(source);
212 let validation_errors = run_validation(&load);
213 let editor_cache = editor::EditorCache::new(source, &load.parse_result);
214
215 Self {
216 source: source.to_string(),
217 parse_result: load.parse_result,
218 directives: load.directives,
219 options: load.options,
220 parse_errors: load.errors,
221 validation_errors,
222 editor_cache,
223 }
224 }
225
226 #[wasm_bindgen(js_name = "isValid")]
228 pub fn is_valid(&self) -> bool {
229 self.parse_errors.is_empty() && self.validation_errors.is_empty()
230 }
231
232 #[wasm_bindgen(js_name = "getErrors")]
234 pub fn get_errors(&self) -> Result<JsValue, JsError> {
235 let mut all_errors = self.parse_errors.clone();
236 all_errors.extend(self.validation_errors.clone());
237 to_js(&all_errors)
238 }
239
240 #[wasm_bindgen(js_name = "getParseErrors")]
242 pub fn get_parse_errors(&self) -> Result<JsValue, JsError> {
243 to_js(&self.parse_errors)
244 }
245
246 #[wasm_bindgen(js_name = "getValidationErrors")]
248 pub fn get_validation_errors(&self) -> Result<JsValue, JsError> {
249 to_js(&self.validation_errors)
250 }
251
252 #[wasm_bindgen(js_name = "getDirectives")]
254 pub fn get_directives(&self) -> Result<JsValue, JsError> {
255 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
256 to_js(&directives)
257 }
258
259 #[wasm_bindgen(js_name = "getOptions")]
261 pub fn get_options(&self) -> Result<JsValue, JsError> {
262 to_js(&self.options)
263 }
264
265 #[wasm_bindgen(js_name = "directiveCount")]
267 pub fn directive_count(&self) -> usize {
268 self.directives.len()
269 }
270
271 #[wasm_bindgen]
273 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
274 if !self.parse_errors.is_empty() {
275 let result = QueryResult {
276 columns: Vec::new(),
277 rows: Vec::new(),
278 errors: self.parse_errors.clone(),
279 };
280 return to_js(&result);
281 }
282 execute_query(
283 &self.directives,
284 query_str,
285 crate::helpers::account_types_from_raw(&self.parse_result.options),
286 )
287 }
288
289 #[wasm_bindgen]
291 pub fn balances(&self) -> Result<JsValue, JsError> {
292 self.query("BALANCES")
293 }
294
295 #[wasm_bindgen]
300 pub fn format(&self) -> Result<JsValue, JsError> {
301 use rustledger_parser::format::format_source_with_parsed;
302
303 if !self.parse_errors.is_empty() {
304 let result = FormatResult {
305 formatted: None,
306 errors: self.parse_errors.clone(),
307 };
308 return to_js(&result);
309 }
310
311 let formatted = format_source_with_parsed(&self.parse_result, &self.source);
318
319 let result = FormatResult {
320 formatted: Some(formatted),
321 errors: Vec::new(),
322 };
323 to_js(&result)
324 }
325
326 #[wasm_bindgen(js_name = "expandPads")]
328 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
329 if !self.parse_errors.is_empty() {
330 let result = PadResult {
331 directives: Vec::new(),
332 padding_transactions: Vec::new(),
333 errors: self.parse_errors.clone(),
334 };
335 return to_js(&result);
336 }
337 execute_expand_pads(&self.directives)
338 }
339
340 #[cfg(feature = "plugins")]
342 #[wasm_bindgen(js_name = "runPlugin")]
343 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
344 if !self.parse_errors.is_empty() {
345 let result = PluginResult {
346 directives: Vec::new(),
347 errors: self.parse_errors.clone(),
348 };
349 return to_js(&result);
350 }
351 execute_plugin(&self.directives, plugin_name)
352 }
353
354 #[wasm_bindgen(js_name = "getCompletions")]
360 pub fn get_completions(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
361 let result =
362 editor::get_completions_cached(&self.source, line, character, &self.editor_cache);
363 to_js(&result)
364 }
365
366 #[wasm_bindgen(js_name = "getHoverInfo")]
368 pub fn get_hover_info(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
369 let result = editor::get_hover_info_cached(
370 &self.source,
371 line,
372 character,
373 &self.parse_result,
374 &self.editor_cache,
375 );
376 to_js(&result)
377 }
378
379 #[wasm_bindgen(js_name = "getDefinition")]
381 pub fn get_definition(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
382 let result = editor::get_definition_cached(
383 &self.source,
384 line,
385 character,
386 &self.parse_result,
387 &self.editor_cache,
388 );
389 to_js(&result)
390 }
391
392 #[wasm_bindgen(js_name = "getDocumentSymbols")]
394 pub fn get_document_symbols(&self) -> Result<JsValue, JsError> {
395 let result = editor::get_document_symbols_cached(&self.parse_result, &self.editor_cache);
396 to_js(&result)
397 }
398
399 #[wasm_bindgen(js_name = "getReferences")]
401 pub fn get_references(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
402 let result = editor::get_references_cached(
403 &self.source,
404 line,
405 character,
406 &self.parse_result,
407 &self.editor_cache,
408 );
409 to_js(&result)
410 }
411
412 #[wasm_bindgen]
421 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
422 let payload = cache::ParsedLedgerPayload {
426 directives: self.directives.clone(),
427 options: self.options.clone(),
428 parse_errors: self.parse_errors.clone(),
429 validation_errors: self.validation_errors.clone(),
430 };
431 cache::serialize_parsed(&payload).map_err(|e| JsError::new(&e))
432 }
433
434 #[wasm_bindgen(js_name = "fromCache")]
445 pub fn from_cache(bytes: &[u8], source: &str) -> Result<Self, JsError> {
446 let mut payload = cache::deserialize_parsed(bytes).map_err(|e| JsError::new(&e))?;
447
448 rustledger_loader::reintern_plain_directives(&mut payload.directives);
450
451 let parse_result = rustledger_parser::parse(source);
453 let editor_cache = editor::EditorCache::new(source, &parse_result);
454
455 Ok(Self {
456 source: source.to_string(),
457 parse_result,
458 directives: payload.directives,
459 options: payload.options,
460 parse_errors: payload.parse_errors,
461 validation_errors: payload.validation_errors,
462 editor_cache,
463 })
464 }
465}
466
467#[wasm_bindgen(skip_typescript)]
492pub struct Ledger {
493 directives: Vec<Directive>,
495 options: LedgerOptions,
497 account_types: rustledger_core::AccountTypes,
502 errors: Vec<Error>,
504 editor_cache: editor::EditorCache,
506}
507
508#[wasm_bindgen]
509impl Ledger {
510 #[wasm_bindgen(js_name = "fromFiles")]
520 pub fn from_files(files: JsValue, entry_point: &str) -> Result<Self, JsError> {
521 use rustledger_loader::{FileSystem, LoadOptions, Loader, VirtualFileSystem, process};
522
523 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
524 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
525
526 if file_map.is_empty() {
527 return Err(JsError::new("Files map cannot be empty"));
528 }
529
530 let vfs = VirtualFileSystem::from_files(file_map);
531
532 if !vfs.exists(Path::new(entry_point)) {
533 return Err(JsError::new(&format!(
534 "Entry point '{entry_point}' not found in files map"
535 )));
536 }
537
538 let mut loader = Loader::new().with_filesystem(Box::new(vfs));
539
540 let load_result = match loader.load(Path::new(entry_point)) {
541 Ok(result) => result,
542 Err(e) => {
543 return Ok(Self {
544 directives: Vec::new(),
545 options: LedgerOptions::default(),
546 account_types: rustledger_core::AccountTypes::default(),
547 errors: vec![Error::new(format!("Load error: {e}"))],
548 editor_cache: editor::EditorCache::from_directives(&[]),
549 });
550 }
551 };
552
553 let options = LedgerOptions {
554 title: load_result.options.title.clone(),
555 operating_currencies: load_result.options.operating_currency.clone(),
556 };
557 let account_types = load_result.options.to_account_types();
558
559 let load_options = LoadOptions {
560 validate: true,
561 ..Default::default()
562 };
563
564 match process(load_result, &load_options) {
565 Ok(ledger) => {
566 let directives: Vec<Directive> =
567 ledger.directives.into_iter().map(|s| s.value).collect();
568 let mut errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
569 for w in &ledger.options.warnings {
572 errors.push(Error::new(format!("[{}] {}", w.code, w.message)));
573 }
574 let editor_cache = editor::EditorCache::from_directives(&directives);
575
576 Ok(Self {
577 directives,
578 options,
579 account_types,
580 errors,
581 editor_cache,
582 })
583 }
584 Err(e) => Ok(Self {
585 directives: Vec::new(),
586 options,
587 account_types,
588 errors: vec![Error::new(format!("Processing error: {e}"))],
589 editor_cache: editor::EditorCache::from_directives(&[]),
590 }),
591 }
592 }
593
594 #[wasm_bindgen(js_name = "isValid")]
596 pub fn is_valid(&self) -> bool {
597 self.errors.is_empty()
598 }
599
600 #[wasm_bindgen(js_name = "getErrors")]
602 pub fn get_errors(&self) -> Result<JsValue, JsError> {
603 to_js(&self.errors)
604 }
605
606 #[wasm_bindgen(js_name = "getDirectives")]
608 pub fn get_directives(&self) -> Result<JsValue, JsError> {
609 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
610 to_js(&directives)
611 }
612
613 #[wasm_bindgen(js_name = "getOptions")]
615 pub fn get_options(&self) -> Result<JsValue, JsError> {
616 to_js(&self.options)
617 }
618
619 #[wasm_bindgen(js_name = "directiveCount")]
621 pub fn directive_count(&self) -> usize {
622 self.directives.len()
623 }
624
625 #[wasm_bindgen]
627 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
628 execute_query(&self.directives, query_str, self.account_types.clone())
629 }
630
631 #[wasm_bindgen]
633 pub fn balances(&self) -> Result<JsValue, JsError> {
634 self.query("BALANCES")
635 }
636
637 #[wasm_bindgen(js_name = "expandPads")]
639 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
640 execute_expand_pads(&self.directives)
641 }
642
643 #[cfg(feature = "plugins")]
645 #[wasm_bindgen(js_name = "runPlugin")]
646 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
647 execute_plugin(&self.directives, plugin_name)
648 }
649
650 #[wasm_bindgen(js_name = "getCompletions")]
655 pub fn get_completions(
656 &self,
657 source: &str,
658 line: u32,
659 character: u32,
660 ) -> Result<JsValue, JsError> {
661 let result = editor::get_completions_cached(source, line, character, &self.editor_cache);
662 to_js(&result)
663 }
664
665 #[wasm_bindgen]
674 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
675 let payload = cache::LedgerPayload {
676 directives: self.directives.clone(),
677 options: self.options.clone(),
678 account_type_names: vec![
679 self.account_types.assets.clone(),
680 self.account_types.liabilities.clone(),
681 self.account_types.equity.clone(),
682 self.account_types.income.clone(),
683 self.account_types.expenses.clone(),
684 ],
685 errors: self.errors.clone(),
686 };
687 cache::serialize_ledger(&payload).map_err(|e| JsError::new(&e))
688 }
689
690 #[wasm_bindgen(js_name = "fromCache")]
697 pub fn from_cache(bytes: &[u8]) -> Result<Self, JsError> {
698 let mut payload = cache::deserialize_ledger(bytes).map_err(|e| JsError::new(&e))?;
699
700 rustledger_loader::reintern_plain_directives(&mut payload.directives);
702
703 let editor_cache = editor::EditorCache::from_directives(&payload.directives);
704
705 let account_types = match <[String; 5]>::try_from(payload.account_type_names) {
706 Ok([assets, liabilities, equity, income, expenses]) => rustledger_core::AccountTypes {
707 assets,
708 liabilities,
709 equity,
710 income,
711 expenses,
712 },
713 Err(_) => rustledger_core::AccountTypes::default(),
717 };
718
719 Ok(Self {
720 directives: payload.directives,
721 options: payload.options,
722 account_types,
723 errors: payload.errors,
724 editor_cache,
725 })
726 }
727}