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 expanded = rustledger_booking::merge_with_padding(directives);
50 let mut executor = Executor::new(&expanded);
51 match executor.execute(&query) {
52 Ok(result) => {
53 let rows: Vec<Vec<_>> = result
54 .rows
55 .iter()
56 .map(|row| row.iter().map(value_to_cell).collect())
57 .collect();
58
59 let query_result = QueryResult {
60 columns: result.columns,
61 rows,
62 errors: Vec::new(),
63 };
64 to_js(&query_result)
65 }
66 Err(e) => {
67 let result = QueryResult {
68 columns: Vec::new(),
69 rows: Vec::new(),
70 errors: vec![Error::new(format!("Query execution error: {e}"))],
71 };
72 to_js(&result)
73 }
74 }
75}
76
77fn execute_expand_pads(directives: &[Directive]) -> Result<JsValue, JsError> {
78 use rustledger_booking::process_pads;
79
80 let pad_result = process_pads(directives);
81
82 let result = PadResult {
83 directives: directives.iter().map(directive_to_json).collect(),
87 padding_transactions: pad_result
88 .padding_transactions
89 .iter()
90 .map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
91 .collect(),
92 errors: pad_result
93 .errors
94 .iter()
95 .map(|e| Error::new(e.message.clone()))
96 .collect(),
97 };
98 to_js(&result)
99}
100
101#[cfg(feature = "plugins")]
102fn execute_plugin(directives: &[Directive], plugin_name: &str) -> Result<JsValue, JsError> {
103 use rustledger_plugin::{
104 NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
105 wrappers_to_directives,
106 };
107
108 let registry = NativePluginRegistry::global();
109 let Some(plugin) = registry.find_regular(plugin_name) else {
113 let result = PluginResult {
114 directives: Vec::new(),
115 errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
116 };
117 return to_js(&result);
118 };
119
120 let wrappers = directives_to_wrappers(directives);
121 let input = PluginInput {
122 directives: wrappers,
123 options: PluginOptions::default(),
124 config: None,
125 };
126
127 let input_dirs = input.directives.clone();
128 let output = plugin.process(input);
129 let materialized = crate::api::materialize_plugin_ops(&input_dirs, &output);
130
131 let output_directives = match wrappers_to_directives(&materialized) {
132 Ok(dirs) => dirs,
133 Err(e) => {
134 let result = PluginResult {
135 directives: Vec::new(),
136 errors: vec![Error::new(format!("Conversion error: {e}"))],
137 };
138 return to_js(&result);
139 }
140 };
141
142 let result = PluginResult {
143 directives: output_directives.iter().map(directive_to_json).collect(),
144 errors: output
145 .errors
146 .iter()
147 .map(|e| match e.severity {
148 rustledger_plugin::PluginErrorSeverity::Warning => {
149 Error::warning(e.message.clone())
150 }
151 rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
152 })
153 .collect(),
154 };
155 to_js(&result)
156}
157
158#[wasm_bindgen(skip_typescript)]
179pub struct ParsedLedger {
180 source: String,
182 parse_result: ParserResult,
184 directives: Vec<Directive>,
186 options: LedgerOptions,
188 parse_errors: Vec<Error>,
190 validation_errors: Vec<Error>,
192 editor_cache: editor::EditorCache,
194}
195
196#[wasm_bindgen]
197impl ParsedLedger {
198 #[wasm_bindgen(constructor)]
202 pub fn new(source: &str) -> Self {
203 let load = load_and_book(source);
204 let validation_errors = run_validation(&load);
205 let editor_cache = editor::EditorCache::new(source, &load.parse_result);
206
207 Self {
208 source: source.to_string(),
209 parse_result: load.parse_result,
210 directives: load.directives,
211 options: load.options,
212 parse_errors: load.errors,
213 validation_errors,
214 editor_cache,
215 }
216 }
217
218 #[wasm_bindgen(js_name = "isValid")]
220 pub fn is_valid(&self) -> bool {
221 self.parse_errors.is_empty() && self.validation_errors.is_empty()
222 }
223
224 #[wasm_bindgen(js_name = "getErrors")]
226 pub fn get_errors(&self) -> Result<JsValue, JsError> {
227 let mut all_errors = self.parse_errors.clone();
228 all_errors.extend(self.validation_errors.clone());
229 to_js(&all_errors)
230 }
231
232 #[wasm_bindgen(js_name = "getParseErrors")]
234 pub fn get_parse_errors(&self) -> Result<JsValue, JsError> {
235 to_js(&self.parse_errors)
236 }
237
238 #[wasm_bindgen(js_name = "getValidationErrors")]
240 pub fn get_validation_errors(&self) -> Result<JsValue, JsError> {
241 to_js(&self.validation_errors)
242 }
243
244 #[wasm_bindgen(js_name = "getDirectives")]
246 pub fn get_directives(&self) -> Result<JsValue, JsError> {
247 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
248 to_js(&directives)
249 }
250
251 #[wasm_bindgen(js_name = "getOptions")]
253 pub fn get_options(&self) -> Result<JsValue, JsError> {
254 to_js(&self.options)
255 }
256
257 #[wasm_bindgen(js_name = "directiveCount")]
259 pub fn directive_count(&self) -> usize {
260 self.directives.len()
261 }
262
263 #[wasm_bindgen]
265 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
266 if !self.parse_errors.is_empty() {
267 let result = QueryResult {
268 columns: Vec::new(),
269 rows: Vec::new(),
270 errors: self.parse_errors.clone(),
271 };
272 return to_js(&result);
273 }
274 execute_query(&self.directives, query_str)
275 }
276
277 #[wasm_bindgen]
279 pub fn balances(&self) -> Result<JsValue, JsError> {
280 self.query("BALANCES")
281 }
282
283 #[wasm_bindgen]
288 pub fn format(&self) -> Result<JsValue, JsError> {
289 use rustledger_parser::format::format_source_with_parsed;
290
291 if !self.parse_errors.is_empty() {
292 let result = FormatResult {
293 formatted: None,
294 errors: self.parse_errors.clone(),
295 };
296 return to_js(&result);
297 }
298
299 let formatted = format_source_with_parsed(&self.parse_result, &self.source);
306
307 let result = FormatResult {
308 formatted: Some(formatted),
309 errors: Vec::new(),
310 };
311 to_js(&result)
312 }
313
314 #[wasm_bindgen(js_name = "expandPads")]
316 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
317 if !self.parse_errors.is_empty() {
318 let result = PadResult {
319 directives: Vec::new(),
320 padding_transactions: Vec::new(),
321 errors: self.parse_errors.clone(),
322 };
323 return to_js(&result);
324 }
325 execute_expand_pads(&self.directives)
326 }
327
328 #[cfg(feature = "plugins")]
330 #[wasm_bindgen(js_name = "runPlugin")]
331 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
332 if !self.parse_errors.is_empty() {
333 let result = PluginResult {
334 directives: Vec::new(),
335 errors: self.parse_errors.clone(),
336 };
337 return to_js(&result);
338 }
339 execute_plugin(&self.directives, plugin_name)
340 }
341
342 #[wasm_bindgen(js_name = "getCompletions")]
348 pub fn get_completions(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
349 let result =
350 editor::get_completions_cached(&self.source, line, character, &self.editor_cache);
351 to_js(&result)
352 }
353
354 #[wasm_bindgen(js_name = "getHoverInfo")]
356 pub fn get_hover_info(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
357 let result = editor::get_hover_info_cached(
358 &self.source,
359 line,
360 character,
361 &self.parse_result,
362 &self.editor_cache,
363 );
364 to_js(&result)
365 }
366
367 #[wasm_bindgen(js_name = "getDefinition")]
369 pub fn get_definition(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
370 let result = editor::get_definition_cached(
371 &self.source,
372 line,
373 character,
374 &self.parse_result,
375 &self.editor_cache,
376 );
377 to_js(&result)
378 }
379
380 #[wasm_bindgen(js_name = "getDocumentSymbols")]
382 pub fn get_document_symbols(&self) -> Result<JsValue, JsError> {
383 let result = editor::get_document_symbols_cached(&self.parse_result, &self.editor_cache);
384 to_js(&result)
385 }
386
387 #[wasm_bindgen(js_name = "getReferences")]
389 pub fn get_references(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
390 let result = editor::get_references_cached(
391 &self.source,
392 line,
393 character,
394 &self.parse_result,
395 &self.editor_cache,
396 );
397 to_js(&result)
398 }
399
400 #[wasm_bindgen]
409 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
410 let payload = cache::ParsedLedgerPayload {
414 directives: self.directives.clone(),
415 options: self.options.clone(),
416 parse_errors: self.parse_errors.clone(),
417 validation_errors: self.validation_errors.clone(),
418 };
419 cache::serialize_parsed(&payload).map_err(|e| JsError::new(&e))
420 }
421
422 #[wasm_bindgen(js_name = "fromCache")]
433 pub fn from_cache(bytes: &[u8], source: &str) -> Result<Self, JsError> {
434 let mut payload = cache::deserialize_parsed(bytes).map_err(|e| JsError::new(&e))?;
435
436 rustledger_loader::reintern_plain_directives(&mut payload.directives);
438
439 let parse_result = rustledger_parser::parse(source);
441 let editor_cache = editor::EditorCache::new(source, &parse_result);
442
443 Ok(Self {
444 source: source.to_string(),
445 parse_result,
446 directives: payload.directives,
447 options: payload.options,
448 parse_errors: payload.parse_errors,
449 validation_errors: payload.validation_errors,
450 editor_cache,
451 })
452 }
453}
454
455#[wasm_bindgen(skip_typescript)]
480pub struct Ledger {
481 directives: Vec<Directive>,
483 options: LedgerOptions,
485 errors: Vec<Error>,
487 editor_cache: editor::EditorCache,
489}
490
491#[wasm_bindgen]
492impl Ledger {
493 #[wasm_bindgen(js_name = "fromFiles")]
503 pub fn from_files(files: JsValue, entry_point: &str) -> Result<Self, JsError> {
504 use rustledger_loader::{FileSystem, LoadOptions, Loader, VirtualFileSystem, process};
505
506 let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
507 .map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
508
509 if file_map.is_empty() {
510 return Err(JsError::new("Files map cannot be empty"));
511 }
512
513 let vfs = VirtualFileSystem::from_files(file_map);
514
515 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));
522
523 let load_result = match loader.load(Path::new(entry_point)) {
524 Ok(result) => result,
525 Err(e) => {
526 return Ok(Self {
527 directives: Vec::new(),
528 options: LedgerOptions::default(),
529 errors: vec![Error::new(format!("Load error: {e}"))],
530 editor_cache: editor::EditorCache::from_directives(&[]),
531 });
532 }
533 };
534
535 let options = LedgerOptions {
536 title: load_result.options.title.clone(),
537 operating_currencies: load_result.options.operating_currency.clone(),
538 };
539
540 let load_options = LoadOptions {
541 validate: true,
542 ..Default::default()
543 };
544
545 match process(load_result, &load_options) {
546 Ok(ledger) => {
547 let directives: Vec<Directive> =
548 ledger.directives.into_iter().map(|s| s.value).collect();
549 let mut errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
550 for w in &ledger.options.warnings {
553 errors.push(Error::new(format!("[{}] {}", w.code, w.message)));
554 }
555 let editor_cache = editor::EditorCache::from_directives(&directives);
556
557 Ok(Self {
558 directives,
559 options,
560 errors,
561 editor_cache,
562 })
563 }
564 Err(e) => Ok(Self {
565 directives: Vec::new(),
566 options,
567 errors: vec![Error::new(format!("Processing error: {e}"))],
568 editor_cache: editor::EditorCache::from_directives(&[]),
569 }),
570 }
571 }
572
573 #[wasm_bindgen(js_name = "isValid")]
575 pub fn is_valid(&self) -> bool {
576 self.errors.is_empty()
577 }
578
579 #[wasm_bindgen(js_name = "getErrors")]
581 pub fn get_errors(&self) -> Result<JsValue, JsError> {
582 to_js(&self.errors)
583 }
584
585 #[wasm_bindgen(js_name = "getDirectives")]
587 pub fn get_directives(&self) -> Result<JsValue, JsError> {
588 let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
589 to_js(&directives)
590 }
591
592 #[wasm_bindgen(js_name = "getOptions")]
594 pub fn get_options(&self) -> Result<JsValue, JsError> {
595 to_js(&self.options)
596 }
597
598 #[wasm_bindgen(js_name = "directiveCount")]
600 pub fn directive_count(&self) -> usize {
601 self.directives.len()
602 }
603
604 #[wasm_bindgen]
606 pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
607 execute_query(&self.directives, query_str)
608 }
609
610 #[wasm_bindgen]
612 pub fn balances(&self) -> Result<JsValue, JsError> {
613 self.query("BALANCES")
614 }
615
616 #[wasm_bindgen(js_name = "expandPads")]
618 pub fn expand_pads(&self) -> Result<JsValue, JsError> {
619 execute_expand_pads(&self.directives)
620 }
621
622 #[cfg(feature = "plugins")]
624 #[wasm_bindgen(js_name = "runPlugin")]
625 pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
626 execute_plugin(&self.directives, plugin_name)
627 }
628
629 #[wasm_bindgen(js_name = "getCompletions")]
634 pub fn get_completions(
635 &self,
636 source: &str,
637 line: u32,
638 character: u32,
639 ) -> Result<JsValue, JsError> {
640 let result = editor::get_completions_cached(source, line, character, &self.editor_cache);
641 to_js(&result)
642 }
643
644 #[wasm_bindgen]
653 pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
654 let payload = cache::LedgerPayload {
655 directives: self.directives.clone(),
656 options: self.options.clone(),
657 errors: self.errors.clone(),
658 };
659 cache::serialize_ledger(&payload).map_err(|e| JsError::new(&e))
660 }
661
662 #[wasm_bindgen(js_name = "fromCache")]
669 pub fn from_cache(bytes: &[u8]) -> Result<Self, JsError> {
670 let mut payload = cache::deserialize_ledger(bytes).map_err(|e| JsError::new(&e))?;
671
672 rustledger_loader::reintern_plain_directives(&mut payload.directives);
674
675 let editor_cache = editor::EditorCache::from_directives(&payload.directives);
676
677 Ok(Self {
678 directives: payload.directives,
679 options: payload.options,
680 errors: payload.errors,
681 editor_cache,
682 })
683 }
684}