use std::collections::HashMap;
use std::path::Path;
use wasm_bindgen::prelude::*;
use rustledger_core::Directive;
use rustledger_loader::{FileSystem, LoadError, LoadResult};
use rustledger_parser::parse as parse_beancount;
use crate::convert::{directive_to_json, value_to_cell};
use crate::helpers::{
extract_options, has_fatal, load_and_book, parse_error_to_wasm, run_validation, to_js,
};
#[cfg(feature = "completions")]
use crate::types::{CompletionJson, CompletionResultJson};
use crate::types::{
Error, FormatResult, Ledger, PadResult, ParseResult, QueryResult, Severity, ValidationResult,
};
#[cfg(feature = "plugins")]
use crate::types::{PluginInfo, PluginResult};
use crate::utils::LineLookup;
fn load_errors_to_errors(load_result: &LoadResult) -> Vec<Error> {
let mut errors = Vec::new();
for load_error in &load_result.errors {
match load_error {
LoadError::ParseErrors {
path,
errors: parse_errors,
} => {
for parse_error in parse_errors {
let span = parse_error.span();
let file = load_result.source_map.get_by_path(path);
let mut err = Error::new(format!("{}: {}", path.display(), parse_error))
.with_code(format!("P{:04}", parse_error.kind_code()))
.with_phase("parse")
.with_hint(parse_error.hint.clone())
.with_file(Some(path.display().to_string()));
if let Some(file) = file {
let (sl, sc) = file.line_col(span.0);
let (el, ec) = file.line_col(span.1);
err = err.with_span((sl as u32, sc as u32), (el as u32, ec as u32));
}
errors.push(err);
}
}
other => {
errors.push(Error::new(other.to_string()));
}
}
}
errors
}
#[wasm_bindgen]
pub fn parse(source: &str) -> Result<JsValue, JsError> {
let result = parse_beancount(source);
let lookup = LineLookup::new(source);
let errors: Vec<Error> = result
.errors
.iter()
.map(|e| parse_error_to_wasm(e, &lookup, None))
.collect();
let options = extract_options(&result.options);
let ledger = Some(Ledger {
directives: result
.directives
.iter()
.map(|spanned| directive_to_json(&spanned.value))
.collect(),
options,
});
let parse_result = ParseResult { ledger, errors };
to_js(&parse_result)
}
#[wasm_bindgen(js_name = "validateSource")]
pub fn validate_source(source: &str) -> Result<JsValue, JsError> {
let load = load_and_book(source);
let validation_errors = run_validation(&load);
let mut errors = load.errors;
errors.extend(validation_errors);
let result = ValidationResult {
valid: !has_fatal(&errors),
errors,
};
to_js(&result)
}
#[wasm_bindgen]
pub fn query(source: &str, query_str: &str) -> Result<JsValue, JsError> {
use rustledger_booking::merge_with_padding;
use rustledger_query::{Executor, parse as parse_query};
let load = load_and_book(source);
if has_fatal(&load.errors) {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: load.errors,
};
return to_js(&result);
}
let warnings = load.errors;
let query = match parse_query(query_str) {
Ok(q) => q,
Err(e) => {
let mut errors = warnings;
errors.push(Error::new(e.to_string()));
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors,
};
return to_js(&result);
}
};
let directives = merge_with_padding(&load.directives);
let mut executor = Executor::new(&directives);
executor.set_account_types(crate::helpers::account_types_from_raw(
&load.parse_result.options,
));
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: warnings,
};
to_js(&query_result)
}
Err(e) => {
let mut errors = warnings;
errors.push(Error::new(format!("Query execution error: {e}")));
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors,
};
to_js(&result)
}
}
}
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen]
pub fn format(source: &str) -> Result<JsValue, JsError> {
use rustledger_parser::format::format_source_with_parsed;
let parse_result = parse_beancount(source);
let lookup = LineLookup::new(source);
if !parse_result.errors.is_empty() {
let result = FormatResult {
formatted: None,
errors: parse_result
.errors
.iter()
.map(|e| parse_error_to_wasm(e, &lookup, None))
.collect(),
};
return to_js(&result);
}
let formatted = format_source_with_parsed(&parse_result, source);
let result = FormatResult {
formatted: Some(formatted),
errors: Vec::new(),
};
to_js(&result)
}
#[wasm_bindgen(js_name = "expandPads")]
pub fn expand_pads(source: &str) -> Result<JsValue, JsError> {
use rustledger_booking::process_pads;
let load = load_and_book(source);
if has_fatal(&load.errors) {
let result = PadResult {
directives: Vec::new(),
padding_transactions: Vec::new(),
errors: load.errors,
};
return to_js(&result);
}
let mut errors = load.errors;
let pad_result = process_pads(&load.directives);
errors.extend(
pad_result
.errors
.iter()
.map(|e| Error::new(e.message.clone())),
);
let result = PadResult {
directives: load.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,
};
to_js(&result)
}
#[cfg(feature = "plugins")]
pub fn materialize_plugin_ops(
input: &[rustledger_plugin::types::DirectiveWrapper],
output: &rustledger_plugin::types::PluginOutput,
) -> Vec<rustledger_plugin::types::DirectiveWrapper> {
let mut out = Vec::with_capacity(output.ops.len());
for op in &output.ops {
match op {
rustledger_plugin::PluginOp::Keep(i) => {
if let Some(w) = input.get(*i) {
out.push(w.clone());
}
}
rustledger_plugin::PluginOp::Modify(_, w) | rustledger_plugin::PluginOp::Insert(w) => {
out.push(w.clone());
}
rustledger_plugin::PluginOp::Delete(_) => {}
}
}
out
}
#[cfg(feature = "plugins")]
#[wasm_bindgen(js_name = "runPlugin")]
pub fn run_plugin(source: &str, plugin_name: &str) -> Result<JsValue, JsError> {
use rustledger_plugin::{
NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
wrappers_to_directives,
};
let load = load_and_book(source);
if has_fatal(&load.errors) {
let result = PluginResult {
directives: Vec::new(),
errors: load.errors,
};
return to_js(&result);
}
let warnings = load.errors;
let registry = NativePluginRegistry::global();
let Some(plugin) = registry.find_regular(plugin_name) else {
let mut errors = warnings;
errors.push(Error::new(format!("Unknown plugin: {plugin_name}")));
let result = PluginResult {
directives: Vec::new(),
errors,
};
return to_js(&result);
};
let wrappers = directives_to_wrappers(&load.directives);
let input = PluginInput {
directives: wrappers,
options: PluginOptions::default(),
config: None,
};
let input_dirs = input.directives.clone();
let output = plugin.process(input);
let materialized_wrappers = materialize_plugin_ops(&input_dirs, &output);
let output_directives = match wrappers_to_directives(&materialized_wrappers) {
Ok(dirs) => dirs,
Err(e) => {
let mut errors = warnings;
errors.push(Error::new(format!("Conversion error: {e}")));
let result = PluginResult {
directives: Vec::new(),
errors,
};
return to_js(&result);
}
};
let mut errors = warnings;
errors.extend(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()),
}));
let result = PluginResult {
directives: output_directives.iter().map(directive_to_json).collect(),
errors,
};
to_js(&result)
}
#[cfg(feature = "plugins")]
#[wasm_bindgen(js_name = "listPlugins")]
pub fn list_plugins() -> Result<JsValue, JsError> {
use rustledger_plugin::NativePluginRegistry;
let registry = NativePluginRegistry::global();
let plugins: Vec<PluginInfo> = registry
.iter()
.map(|p| PluginInfo {
name: p.name().to_string(),
description: p.description().to_string(),
})
.collect();
to_js(&plugins)
}
#[wasm_bindgen]
pub fn balances(source: &str) -> Result<JsValue, JsError> {
query(source, "BALANCES")
}
#[cfg(feature = "completions")]
#[wasm_bindgen(js_name = "bqlCompletions")]
pub fn bql_completions(partial_query: &str, cursor_pos: usize) -> Result<JsValue, JsError> {
use rustledger_query::completions;
let result = completions::complete(partial_query, cursor_pos);
let json_result = CompletionResultJson {
completions: result
.completions
.into_iter()
.map(|c| CompletionJson {
text: c.text,
category: c.category.as_str().to_string(),
description: c.description,
})
.collect(),
context: format!("{:?}", result.context),
};
to_js(&json_result)
}
#[wasm_bindgen(js_name = "parseMultiFile")]
pub fn parse_multi_file(files: JsValue, entry_point: &str) -> Result<JsValue, JsError> {
use rustledger_loader::{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) => {
let result = ParseResult {
ledger: None,
errors: vec![Error::new(format!("Load error: {e}"))],
};
return to_js(&result);
}
};
let mut errors = load_errors_to_errors(&load_result);
let options = crate::types::LedgerOptions {
title: load_result.options.title.clone(),
operating_currencies: load_result.options.operating_currency.clone(),
};
let directives: Vec<Directive> = if errors.is_empty() {
let process_options = LoadOptions {
validate: false,
..Default::default()
};
match process(load_result, &process_options) {
Ok(ledger) => {
errors.extend(ledger.errors.into_iter().map(Error::from));
ledger.directives.into_iter().map(|s| s.value).collect()
}
Err(e) => {
let result = ParseResult {
ledger: None,
errors: vec![Error::new(format!("Processing error: {e}"))],
};
return to_js(&result);
}
}
} else {
load_result
.directives
.into_iter()
.map(|s| s.value)
.collect()
};
let ledger = Some(Ledger {
directives: directives.iter().map(directive_to_json).collect(),
options,
});
let result = ParseResult { ledger, errors };
to_js(&result)
}
#[wasm_bindgen(js_name = "validateMultiFile")]
pub fn validate_multi_file(files: JsValue, entry_point: &str) -> Result<JsValue, JsError> {
use rustledger_loader::{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) => {
let result = ValidationResult {
valid: false,
errors: vec![Error::new(format!("Load error: {e}"))],
};
return to_js(&result);
}
};
let parse_errors = load_errors_to_errors(&load_result);
if !parse_errors.is_empty() {
let result = ValidationResult {
valid: false,
errors: parse_errors,
};
return to_js(&result);
}
let options = LoadOptions {
validate: true,
..Default::default()
};
let ledger = match process(load_result, &options) {
Ok(ledger) => ledger,
Err(e) => {
let result = ValidationResult {
valid: false,
errors: vec![Error::new(format!("Processing error: {e}"))],
};
return to_js(&result);
}
};
let errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
let result = ValidationResult {
valid: !has_fatal(&errors),
errors,
};
to_js(&result)
}
#[wasm_bindgen(js_name = "queryMultiFile")]
pub fn query_multi_file(
files: JsValue,
entry_point: &str,
query_str: &str,
) -> Result<JsValue, JsError> {
use rustledger_booking::merge_with_padding;
use rustledger_loader::{LoadOptions, Loader, VirtualFileSystem, process};
use rustledger_query::{Executor, parse as parse_query};
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) => {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: vec![Error::new(format!("Load error: {e}"))],
};
return to_js(&result);
}
};
let parse_errors = load_errors_to_errors(&load_result);
if !parse_errors.is_empty() {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: parse_errors,
};
return to_js(&result);
}
let options = LoadOptions {
validate: false,
..Default::default()
};
let ledger = match process(load_result, &options) {
Ok(ledger) => ledger,
Err(e) => {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors: vec![Error::new(format!("Processing error: {e}"))],
};
return to_js(&result);
}
};
let errors: Vec<Error> = ledger.errors.into_iter().map(Error::from).collect();
let has_errors = errors.iter().any(|e| e.severity == Severity::Error);
if has_errors {
let result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
errors,
};
return to_js(&result);
}
let account_types = ledger.options.to_account_types();
let booked_directives: Vec<_> = ledger.directives.into_iter().map(|s| s.value).collect();
let directives = merge_with_padding(&booked_directives);
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);
executor.set_account_types(account_types);
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)
}
}
}
#[wasm_bindgen(js_name = "hashSources")]
#[allow(clippy::needless_pass_by_value)] pub fn hash_sources(sources: Vec<String>) -> String {
let refs: Vec<&str> = sources.iter().map(String::as_str).collect();
crate::cache::hash_sources(&refs)
}