use std::collections::HashMap;
use std::path::Path;
use wasm_bindgen::prelude::*;
use rustledger_core::Directive;
use rustledger_parser::ParseResult as ParserResult;
use crate::cache;
use crate::convert::directive_to_json;
use crate::editor;
use crate::helpers::{load_and_book, run_validation, to_js};
#[cfg(feature = "plugins")]
use crate::types::PluginResult;
use crate::types::{Error, FormatResult, LedgerOptions, PadResult, QueryResult};
fn execute_query(directives: &[Directive], query_str: &str) -> Result<JsValue, JsError> {
use crate::convert::value_to_cell;
use rustledger_query::{Executor, parse as parse_query};
let query = match parse_query(query_str) {
Ok(q) => q,
Err(e) => {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: vec![Error::new(e.to_string())],
};
return to_js(&result);
}
};
let mut executor = Executor::new(directives);
match executor.execute(&query) {
Ok(result) => {
let rows: Vec<Vec<_>> = result
.rows
.iter()
.map(|row| row.iter().map(value_to_cell).collect())
.collect();
let query_result = QueryResult {
columns: result.columns,
rows,
errors: Vec::new(),
};
to_js(&query_result)
}
Err(e) => {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: vec![Error::new(format!("Query execution error: {e}"))],
};
to_js(&result)
}
}
}
fn execute_expand_pads(directives: &[Directive]) -> Result<JsValue, JsError> {
use rustledger_booking::process_pads;
let pad_result = process_pads(directives);
let result = PadResult {
directives: pad_result
.directives
.iter()
.map(directive_to_json)
.collect(),
padding_transactions: pad_result
.padding_transactions
.iter()
.map(|txn| directive_to_json(&Directive::Transaction(txn.clone())))
.collect(),
errors: pad_result
.errors
.iter()
.map(|e| Error::new(e.message.clone()))
.collect(),
};
to_js(&result)
}
#[cfg(feature = "plugins")]
fn execute_plugin(directives: &[Directive], plugin_name: &str) -> Result<JsValue, JsError> {
use rustledger_plugin::{
NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
wrappers_to_directives,
};
let registry = NativePluginRegistry::new();
let Some(plugin) = registry.find(plugin_name) else {
let result = PluginResult {
directives: Vec::new(),
errors: vec![Error::new(format!("Unknown plugin: {plugin_name}"))],
};
return to_js(&result);
};
let wrappers = directives_to_wrappers(directives);
let input = PluginInput {
directives: wrappers,
options: PluginOptions::default(),
config: None,
};
let output = plugin.process(input);
let output_directives = match wrappers_to_directives(&output.directives) {
Ok(dirs) => dirs,
Err(e) => {
let result = PluginResult {
directives: Vec::new(),
errors: vec![Error::new(format!("Conversion error: {e}"))],
};
return to_js(&result);
}
};
let result = PluginResult {
directives: output_directives.iter().map(directive_to_json).collect(),
errors: output
.errors
.iter()
.map(|e| match e.severity {
rustledger_plugin::PluginErrorSeverity::Warning => {
Error::warning(e.message.clone())
}
rustledger_plugin::PluginErrorSeverity::Error => Error::new(e.message.clone()),
})
.collect(),
};
to_js(&result)
}
#[wasm_bindgen(skip_typescript)]
pub struct ParsedLedger {
source: String,
parse_result: ParserResult,
directives: Vec<Directive>,
options: LedgerOptions,
parse_errors: Vec<Error>,
validation_errors: Vec<Error>,
editor_cache: editor::EditorCache,
}
#[wasm_bindgen]
impl ParsedLedger {
#[wasm_bindgen(constructor)]
pub fn new(source: &str) -> Self {
let load = load_and_book(source);
let validation_errors = run_validation(&load);
let editor_cache = editor::EditorCache::new(source, &load.parse_result);
Self {
source: source.to_string(),
parse_result: load.parse_result,
directives: load.directives,
options: load.options,
parse_errors: load.errors,
validation_errors,
editor_cache,
}
}
#[wasm_bindgen(js_name = "isValid")]
pub fn is_valid(&self) -> bool {
self.parse_errors.is_empty() && self.validation_errors.is_empty()
}
#[wasm_bindgen(js_name = "getErrors")]
pub fn get_errors(&self) -> Result<JsValue, JsError> {
let mut all_errors = self.parse_errors.clone();
all_errors.extend(self.validation_errors.clone());
to_js(&all_errors)
}
#[wasm_bindgen(js_name = "getParseErrors")]
pub fn get_parse_errors(&self) -> Result<JsValue, JsError> {
to_js(&self.parse_errors)
}
#[wasm_bindgen(js_name = "getValidationErrors")]
pub fn get_validation_errors(&self) -> Result<JsValue, JsError> {
to_js(&self.validation_errors)
}
#[wasm_bindgen(js_name = "getDirectives")]
pub fn get_directives(&self) -> Result<JsValue, JsError> {
let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
to_js(&directives)
}
#[wasm_bindgen(js_name = "getOptions")]
pub fn get_options(&self) -> Result<JsValue, JsError> {
to_js(&self.options)
}
#[wasm_bindgen(js_name = "directiveCount")]
pub fn directive_count(&self) -> usize {
self.directives.len()
}
#[wasm_bindgen]
pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
if !self.parse_errors.is_empty() {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: self.parse_errors.clone(),
};
return to_js(&result);
}
execute_query(&self.directives, query_str)
}
#[wasm_bindgen]
pub fn balances(&self) -> Result<JsValue, JsError> {
self.query("BALANCES")
}
#[wasm_bindgen]
pub fn format(&self) -> Result<JsValue, JsError> {
use rustledger_core::{FormatConfig, format_directive};
if !self.parse_errors.is_empty() {
let result = FormatResult {
formatted: None,
errors: self.parse_errors.clone(),
};
return to_js(&result);
}
let config = FormatConfig::default();
let mut formatted = String::new();
for directive in &self.directives {
formatted.push_str(&format_directive(directive, &config));
formatted.push('\n');
}
let result = FormatResult {
formatted: Some(formatted),
errors: Vec::new(),
};
to_js(&result)
}
#[wasm_bindgen(js_name = "expandPads")]
pub fn expand_pads(&self) -> Result<JsValue, JsError> {
if !self.parse_errors.is_empty() {
let result = PadResult {
directives: Vec::new(),
padding_transactions: Vec::new(),
errors: self.parse_errors.clone(),
};
return to_js(&result);
}
execute_expand_pads(&self.directives)
}
#[cfg(feature = "plugins")]
#[wasm_bindgen(js_name = "runPlugin")]
pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
if !self.parse_errors.is_empty() {
let result = PluginResult {
directives: Vec::new(),
errors: self.parse_errors.clone(),
};
return to_js(&result);
}
execute_plugin(&self.directives, plugin_name)
}
#[wasm_bindgen(js_name = "getCompletions")]
pub fn get_completions(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
let result =
editor::get_completions_cached(&self.source, line, character, &self.editor_cache);
to_js(&result)
}
#[wasm_bindgen(js_name = "getHoverInfo")]
pub fn get_hover_info(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
let result = editor::get_hover_info_cached(
&self.source,
line,
character,
&self.parse_result,
&self.editor_cache,
);
to_js(&result)
}
#[wasm_bindgen(js_name = "getDefinition")]
pub fn get_definition(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
let result = editor::get_definition_cached(
&self.source,
line,
character,
&self.parse_result,
&self.editor_cache,
);
to_js(&result)
}
#[wasm_bindgen(js_name = "getDocumentSymbols")]
pub fn get_document_symbols(&self) -> Result<JsValue, JsError> {
let result = editor::get_document_symbols_cached(&self.parse_result, &self.editor_cache);
to_js(&result)
}
#[wasm_bindgen(js_name = "getReferences")]
pub fn get_references(&self, line: u32, character: u32) -> Result<JsValue, JsError> {
let result = editor::get_references_cached(
&self.source,
line,
character,
&self.parse_result,
&self.editor_cache,
);
to_js(&result)
}
#[wasm_bindgen]
pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
let payload = cache::ParsedLedgerPayload {
directives: self.directives.clone(),
options: self.options.clone(),
parse_errors: self.parse_errors.clone(),
validation_errors: self.validation_errors.clone(),
};
cache::serialize_parsed(&payload).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "fromCache")]
pub fn from_cache(bytes: &[u8], source: &str) -> Result<Self, JsError> {
let mut payload = cache::deserialize_parsed(bytes).map_err(|e| JsError::new(&e))?;
rustledger_loader::reintern_plain_directives(&mut payload.directives);
let parse_result = rustledger_parser::parse(source);
let editor_cache = editor::EditorCache::new(source, &parse_result);
Ok(Self {
source: source.to_string(),
parse_result,
directives: payload.directives,
options: payload.options,
parse_errors: payload.parse_errors,
validation_errors: payload.validation_errors,
editor_cache,
})
}
}
#[wasm_bindgen(skip_typescript)]
pub struct Ledger {
directives: Vec<Directive>,
options: LedgerOptions,
errors: Vec<Error>,
editor_cache: editor::EditorCache,
}
#[wasm_bindgen]
impl Ledger {
#[wasm_bindgen(js_name = "fromFiles")]
pub fn from_files(files: JsValue, entry_point: &str) -> Result<Self, JsError> {
use rustledger_loader::{FileSystem, LoadOptions, Loader, VirtualFileSystem, process};
let file_map: HashMap<String, String> = serde_wasm_bindgen::from_value(files)
.map_err(|e| JsError::new(&format!("Invalid files object: {e}")))?;
if file_map.is_empty() {
return Err(JsError::new("Files map cannot be empty"));
}
let vfs = VirtualFileSystem::from_files(file_map);
if !vfs.exists(Path::new(entry_point)) {
return Err(JsError::new(&format!(
"Entry point '{entry_point}' not found in files map"
)));
}
let mut loader = Loader::new().with_filesystem(Box::new(vfs));
let load_result = match loader.load(Path::new(entry_point)) {
Ok(result) => result,
Err(e) => {
return Ok(Self {
directives: Vec::new(),
options: LedgerOptions::default(),
errors: vec![Error::new(format!("Load error: {e}"))],
editor_cache: editor::EditorCache::from_directives(&[]),
});
}
};
let options = LedgerOptions {
title: load_result.options.title.clone(),
operating_currencies: load_result.options.operating_currency.clone(),
};
let load_options = LoadOptions {
validate: true,
..Default::default()
};
match process(load_result, &load_options) {
Ok(ledger) => {
let directives: Vec<Directive> =
ledger.directives.into_iter().map(|s| s.value).collect();
let mut errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
for w in &ledger.options.warnings {
errors.push(Error::new(format!("[{}] {}", w.code, w.message)));
}
let editor_cache = editor::EditorCache::from_directives(&directives);
Ok(Self {
directives,
options,
errors,
editor_cache,
})
}
Err(e) => Ok(Self {
directives: Vec::new(),
options,
errors: vec![Error::new(format!("Processing error: {e}"))],
editor_cache: editor::EditorCache::from_directives(&[]),
}),
}
}
#[wasm_bindgen(js_name = "isValid")]
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
#[wasm_bindgen(js_name = "getErrors")]
pub fn get_errors(&self) -> Result<JsValue, JsError> {
to_js(&self.errors)
}
#[wasm_bindgen(js_name = "getDirectives")]
pub fn get_directives(&self) -> Result<JsValue, JsError> {
let directives: Vec<_> = self.directives.iter().map(directive_to_json).collect();
to_js(&directives)
}
#[wasm_bindgen(js_name = "getOptions")]
pub fn get_options(&self) -> Result<JsValue, JsError> {
to_js(&self.options)
}
#[wasm_bindgen(js_name = "directiveCount")]
pub fn directive_count(&self) -> usize {
self.directives.len()
}
#[wasm_bindgen]
pub fn query(&self, query_str: &str) -> Result<JsValue, JsError> {
execute_query(&self.directives, query_str)
}
#[wasm_bindgen]
pub fn balances(&self) -> Result<JsValue, JsError> {
self.query("BALANCES")
}
#[wasm_bindgen(js_name = "expandPads")]
pub fn expand_pads(&self) -> Result<JsValue, JsError> {
execute_expand_pads(&self.directives)
}
#[cfg(feature = "plugins")]
#[wasm_bindgen(js_name = "runPlugin")]
pub fn run_plugin(&self, plugin_name: &str) -> Result<JsValue, JsError> {
execute_plugin(&self.directives, plugin_name)
}
#[wasm_bindgen(js_name = "getCompletions")]
pub fn get_completions(
&self,
source: &str,
line: u32,
character: u32,
) -> Result<JsValue, JsError> {
let result = editor::get_completions_cached(source, line, character, &self.editor_cache);
to_js(&result)
}
#[wasm_bindgen]
pub fn serialize(&self) -> Result<Vec<u8>, JsError> {
let payload = cache::LedgerPayload {
directives: self.directives.clone(),
options: self.options.clone(),
errors: self.errors.clone(),
};
cache::serialize_ledger(&payload).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "fromCache")]
pub fn from_cache(bytes: &[u8]) -> Result<Self, JsError> {
let mut payload = cache::deserialize_ledger(bytes).map_err(|e| JsError::new(&e))?;
rustledger_loader::reintern_plain_directives(&mut payload.directives);
let editor_cache = editor::EditorCache::from_directives(&payload.directives);
Ok(Self {
directives: payload.directives,
options: payload.options,
errors: payload.errors,
editor_cache,
})
}
}