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(directives: &[Directive], query_str: &str) -> Result<JsValue, JsError> {
27 use crate::convert::value_to_cell;
28 use rustledger_query::{Executor, parse as parse_query};
29
30 let query = match parse_query(query_str) {
31 Ok(q) => q,
32 Err(e) => {
33 let result = QueryResult {
34 columns: Vec::new(),
35 rows: Vec::new(),
36 errors: vec![Error::new(e.to_string())],
37 };
38 return to_js(&result);
39 }
40 };
41
42 let mut executor = Executor::new(directives);
43 match executor.execute(&query) {
44 Ok(result) => {
45 let rows: Vec<Vec<_>> = result
46 .rows
47 .iter()
48 .map(|row| row.iter().map(value_to_cell).collect())
49 .collect();
50
51 let query_result = QueryResult {
52 columns: result.columns,
53 rows,
54 errors: Vec::new(),
55 };
56 to_js(&query_result)
57 }
58 Err(e) => {
59 let result = QueryResult {
60 columns: Vec::new(),
61 rows: Vec::new(),
62 errors: vec![Error::new(format!("Query execution error: {e}"))],
63 };
64 to_js(&result)
65 }
66 }
67}
68
69fn execute_expand_pads(directives: &[Directive]) -> Result<JsValue, JsError> {
70 use rustledger_booking::process_pads;
71
72 let pad_result = process_pads(directives);
73
74 let result = PadResult {
75 directives: pad_result
76 .directives
77 .iter()
78 .map(directive_to_json)
79 .collect(),
80 padding_transactions: pad_result
81 .padding_transactions
82 .iter()
83 .map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
84 .collect(),
85 errors: pad_result
86 .errors
87 .iter()
88 .map(|e| Error::new(e.message.clone()))
89 .collect(),
90 };
91 to_js(&result)
92}
93
94#[cfg(feature = "plugins")]
95fn execute_plugin(directives: &[Directive], plugin_name: &str) -> Result<JsValue, JsError> {
96 use rustledger_plugin::{
97 NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
98 wrappers_to_directives,
99 };
100
101 let registry = NativePluginRegistry::new();
102 let Some(plugin) = registry.find(plugin_name) else {
103 let result = PluginResult {
104 directives: Vec::new(),
105 errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
106 };
107 return to_js(&result);
108 };
109
110 let wrappers = directives_to_wrappers(directives);
111 let input = PluginInput {
112 directives: wrappers,
113 options: PluginOptions::default(),
114 config: None,
115 };
116
117 let output = plugin.process(input);
118
119 let output_directives = match wrappers_to_directives(&output.directives) {
120 Ok(dirs) => dirs,
121 Err(e) => {
122 let result = PluginResult {
123 directives: Vec::new(),
124 errors: vec![Error::new(format!("Conversion error: {e}"))],
125 };
126 return to_js(&result);
127 }
128 };
129
130 let result = PluginResult {
131 directives: output_directives.iter().map(directive_to_json).collect(),
132 errors: output
133 .errors
134 .iter()
135 .map(|e| match e.severity {
136 rustledger_plugin::PluginErrorSeverity::Warning => {
137 Error::warning(e.message.clone())
138 }
139 rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
140 })
141 .collect(),
142 };
143 to_js(&result)
144}
145
146#[wasm_bindgen(skip_typescript)]
167pub struct ParsedLedger {
168 source: String,
170 parse_result: ParserResult,
172 directives: Vec<Directive>,
174 options: LedgerOptions,
176 parse_errors: Vec<Error>,
178 validation_errors: Vec<Error>,
180 editor_cache: editor::EditorCache,
182}
183
184#[wasm_bindgen]
185impl ParsedLedger {
186 #[wasm_bindgen(constructor)]
190 pub fn new(source: &str) -> Self {
191 let load = load_and_book(source);
192 let validation_errors = run_validation(&load);
193 let editor_cache = editor::EditorCache::new(source, &load.parse_result);
194
195 Self {
196 source: source.to_string(),
197 parse_result: load.parse_result,
198 directives: load.directives,
199 options: load.options,
200 parse_errors: load.errors,
201 validation_errors,
202 editor_cache,
203 }
204 }
205
206 #[wasm_bindgen(js_name = "isValid")]
208 pub fn is_valid(&self) -> bool {
209 self.parse_errors.is_empty() && self.validation_errors.is_empty()
210 }
211
212 #[wasm_bindgen(js_name = "getErrors")]
214 pub fn get_errors(&self) -> Result<JsValue, JsError> {
215 let mut all_errors = self.parse_errors.clone();
216 all_errors.extend(self.validation_errors.clone());
217 to_js(&all_errors)
218 }
219
220 #[wasm_bindgen(js_name = "getParseErrors")]
222 pub fn get_parse_errors(&self) -> Result<JsValue, JsError> {
223 to_js(&self.parse_errors)
224 }
225
226 #[wasm_bindgen(js_name = "getValidationErrors")]
228 pub fn get_validation_errors(&self) -> Result<JsValue, JsError> {
229 to_js(&self.validation_errors)
230 }
231
232 #[wasm_bindgen(js_name = "getDirectives")]
234 pub fn get_directives(&self) -> Result<JsValue, JsError> {
235 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
236 to_js(&directives)
237 }
238
239 #[wasm_bindgen(js_name = "getOptions")]
241 pub fn get_options(&self) -> Result<JsValue, JsError> {
242 to_js(&self.options)
243 }
244
245 #[wasm_bindgen(js_name = "directiveCount")]
247 pub fn directive_count(&self) -> usize {
248 self.directives.len()
249 }
250
251 #[wasm_bindgen]
253 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
254 if !self.parse_errors.is_empty() {
255 let result = QueryResult {
256 columns: Vec::new(),
257 rows: Vec::new(),
258 errors: self.parse_errors.clone(),
259 };
260 return to_js(&result);
261 }
262 execute_query(&self.directives, query_str)
263 }
264
265 #[wasm_bindgen]
267 pub fn balances(&self) -> Result<JsValue, JsError> {
268 self.query("BALANCES")
269 }
270
271 #[wasm_bindgen]
273 pub fn format(&self) -> Result<JsValue, JsError> {
274 use rustledger_core::{FormatConfig, format_directive};
275
276 if !self.parse_errors.is_empty() {
277 let result = FormatResult {
278 formatted: None,
279 errors: self.parse_errors.clone(),
280 };
281 return to_js(&result);
282 }
283
284 let config = FormatConfig::default();
285 let mut formatted = String::new();
286
287 for directive in &self.directives {
288 formatted.push_str(&format_directive(directive, &config));
289 formatted.push('\n');
290 }
291
292 let result = FormatResult {
293 formatted: Some(formatted),
294 errors: Vec::new(),
295 };
296 to_js(&result)
297 }
298
299 #[wasm_bindgen(js_name = "expandPads")]
301 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
302 if !self.parse_errors.is_empty() {
303 let result = PadResult {
304 directives: Vec::new(),
305 padding_transactions: Vec::new(),
306 errors: self.parse_errors.clone(),
307 };
308 return to_js(&result);
309 }
310 execute_expand_pads(&self.directives)
311 }
312
313 #[cfg(feature = "plugins")]
315 #[wasm_bindgen(js_name = "runPlugin")]
316 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
317 if !self.parse_errors.is_empty() {
318 let result = PluginResult {
319 directives: Vec::new(),
320 errors: self.parse_errors.clone(),
321 };
322 return to_js(&result);
323 }
324 execute_plugin(&self.directives, plugin_name)
325 }
326
327 #[wasm_bindgen(js_name = "getCompletions")]
333 pub fn get_completions(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
334 let result =
335 editor::get_completions_cached(&self.source, line, character, &self.editor_cache);
336 to_js(&result)
337 }
338
339 #[wasm_bindgen(js_name = "getHoverInfo")]
341 pub fn get_hover_info(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
342 let result = editor::get_hover_info_cached(
343 &self.source,
344 line,
345 character,
346 &self.parse_result,
347 &self.editor_cache,
348 );
349 to_js(&result)
350 }
351
352 #[wasm_bindgen(js_name = "getDefinition")]
354 pub fn get_definition(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
355 let result = editor::get_definition_cached(
356 &self.source,
357 line,
358 character,
359 &self.parse_result,
360 &self.editor_cache,
361 );
362 to_js(&result)
363 }
364
365 #[wasm_bindgen(js_name = "getDocumentSymbols")]
367 pub fn get_document_symbols(&self) -> Result<JsValue, JsError> {
368 let result = editor::get_document_symbols_cached(&self.parse_result, &self.editor_cache);
369 to_js(&result)
370 }
371
372 #[wasm_bindgen(js_name = "getReferences")]
374 pub fn get_references(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
375 let result = editor::get_references_cached(
376 &self.source,
377 line,
378 character,
379 &self.parse_result,
380 &self.editor_cache,
381 );
382 to_js(&result)
383 }
384
385 #[wasm_bindgen]
394 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
395 let payload = cache::ParsedLedgerPayload {
399 directives: self.directives.clone(),
400 options: self.options.clone(),
401 parse_errors: self.parse_errors.clone(),
402 validation_errors: self.validation_errors.clone(),
403 };
404 cache::serialize_parsed(&payload).map_err(|e| JsError::new(&e))
405 }
406
407 #[wasm_bindgen(js_name = "fromCache")]
418 pub fn from_cache(bytes: &[u8], source: &str) -> Result<Self, JsError> {
419 let mut payload = cache::deserialize_parsed(bytes).map_err(|e| JsError::new(&e))?;
420
421 rustledger_loader::reintern_plain_directives(&mut payload.directives);
423
424 let parse_result = rustledger_parser::parse(source);
426 let editor_cache = editor::EditorCache::new(source, &parse_result);
427
428 Ok(Self {
429 source: source.to_string(),
430 parse_result,
431 directives: payload.directives,
432 options: payload.options,
433 parse_errors: payload.parse_errors,
434 validation_errors: payload.validation_errors,
435 editor_cache,
436 })
437 }
438}
439
440#[wasm_bindgen(skip_typescript)]
465pub struct Ledger {
466 directives: Vec<Directive>,
468 options: LedgerOptions,
470 errors: Vec<Error>,
472 editor_cache: editor::EditorCache,
474}
475
476#[wasm_bindgen]
477impl Ledger {
478 #[wasm_bindgen(js_name = "fromFiles")]
488 pub fn from_files(files: JsValue, entry_point: &str) -> Result<Self, JsError> {
489 use rustledger_loader::{FileSystem, LoadOptions, Loader, VirtualFileSystem, process};
490
491 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
492 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
493
494 if file_map.is_empty() {
495 return Err(JsError::new("Files map cannot be empty"));
496 }
497
498 let vfs = VirtualFileSystem::from_files(file_map);
499
500 if !vfs.exists(Path::new(entry_point)) {
501 return Err(JsError::new(&format!(
502 "Entry point '{entry_point}' not found in files map"
503 )));
504 }
505
506 let mut loader = Loader::new().with_filesystem(Box::new(vfs));
507
508 let load_result = match loader.load(Path::new(entry_point)) {
509 Ok(result) => result,
510 Err(e) => {
511 return Ok(Self {
512 directives: Vec::new(),
513 options: LedgerOptions::default(),
514 errors: vec![Error::new(format!("Load error: {e}"))],
515 editor_cache: editor::EditorCache::from_directives(&[]),
516 });
517 }
518 };
519
520 let options = LedgerOptions {
521 title: load_result.options.title.clone(),
522 operating_currencies: load_result.options.operating_currency.clone(),
523 };
524
525 let load_options = LoadOptions {
526 validate: true,
527 ..Default::default()
528 };
529
530 match process(load_result, &load_options) {
531 Ok(ledger) => {
532 let directives: Vec<Directive> =
533 ledger.directives.into_iter().map(|s| s.value).collect();
534 let mut errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
535 for w in &ledger.options.warnings {
538 errors.push(Error::new(format!("[{}] {}", w.code, w.message)));
539 }
540 let editor_cache = editor::EditorCache::from_directives(&directives);
541
542 Ok(Self {
543 directives,
544 options,
545 errors,
546 editor_cache,
547 })
548 }
549 Err(e) => Ok(Self {
550 directives: Vec::new(),
551 options,
552 errors: vec![Error::new(format!("Processing error: {e}"))],
553 editor_cache: editor::EditorCache::from_directives(&[]),
554 }),
555 }
556 }
557
558 #[wasm_bindgen(js_name = "isValid")]
560 pub fn is_valid(&self) -> bool {
561 self.errors.is_empty()
562 }
563
564 #[wasm_bindgen(js_name = "getErrors")]
566 pub fn get_errors(&self) -> Result<JsValue, JsError> {
567 to_js(&self.errors)
568 }
569
570 #[wasm_bindgen(js_name = "getDirectives")]
572 pub fn get_directives(&self) -> Result<JsValue, JsError> {
573 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
574 to_js(&directives)
575 }
576
577 #[wasm_bindgen(js_name = "getOptions")]
579 pub fn get_options(&self) -> Result<JsValue, JsError> {
580 to_js(&self.options)
581 }
582
583 #[wasm_bindgen(js_name = "directiveCount")]
585 pub fn directive_count(&self) -> usize {
586 self.directives.len()
587 }
588
589 #[wasm_bindgen]
591 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
592 execute_query(&self.directives, query_str)
593 }
594
595 #[wasm_bindgen]
597 pub fn balances(&self) -> Result<JsValue, JsError> {
598 self.query("BALANCES")
599 }
600
601 #[wasm_bindgen(js_name = "expandPads")]
603 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
604 execute_expand_pads(&self.directives)
605 }
606
607 #[cfg(feature = "plugins")]
609 #[wasm_bindgen(js_name = "runPlugin")]
610 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
611 execute_plugin(&self.directives, plugin_name)
612 }
613
614 #[wasm_bindgen(js_name = "getCompletions")]
619 pub fn get_completions(
620 &self,
621 source: &str,
622 line: u32,
623 character: u32,
624 ) -> Result<JsValue, JsError> {
625 let result = editor::get_completions_cached(source, line, character, &self.editor_cache);
626 to_js(&result)
627 }
628
629 #[wasm_bindgen]
638 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
639 let payload = cache::LedgerPayload {
640 directives: self.directives.clone(),
641 options: self.options.clone(),
642 errors: self.errors.clone(),
643 };
644 cache::serialize_ledger(&payload).map_err(|e| JsError::new(&e))
645 }
646
647 #[wasm_bindgen(js_name = "fromCache")]
654 pub fn from_cache(bytes: &[u8]) -> Result<Self, JsError> {
655 let mut payload = cache::deserialize_ledger(bytes).map_err(|e| JsError::new(&e))?;
656
657 rustledger_loader::reintern_plain_directives(&mut payload.directives);
659
660 let editor_cache = editor::EditorCache::from_directives(&payload.directives);
661
662 Ok(Self {
663 directives: payload.directives,
664 options: payload.options,
665 errors: payload.errors,
666 editor_cache,
667 })
668 }
669}