use crate::{LoadError, LoadResult, Options, Plugin, SourceMap};
use rustledger_core::{BookingMethod, Directive, DisplayContext};
use rustledger_parser::Spanned;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct LoadOptions {
pub booking_method: BookingMethod,
pub run_plugins: bool,
pub auto_accounts: bool,
pub extra_plugins: Vec<String>,
pub extra_plugin_configs: Vec<Option<String>>,
pub validate: bool,
pub path_security: bool,
}
impl Default for LoadOptions {
fn default() -> Self {
Self {
booking_method: BookingMethod::Strict,
run_plugins: true,
auto_accounts: false,
extra_plugins: Vec::new(),
extra_plugin_configs: Vec::new(),
validate: true,
path_security: false,
}
}
}
impl LoadOptions {
#[must_use]
pub const fn raw() -> Self {
Self {
booking_method: BookingMethod::Strict,
run_plugins: false,
auto_accounts: false,
extra_plugins: Vec::new(),
extra_plugin_configs: Vec::new(),
validate: false,
path_security: false,
}
}
}
#[derive(Debug, Error)]
pub enum ProcessError {
#[error("loading failed: {0}")]
Load(#[from] LoadError),
#[cfg(feature = "booking")]
#[error("booking error: {message}")]
Booking {
message: String,
date: rustledger_core::NaiveDate,
narration: String,
},
#[cfg(feature = "plugins")]
#[error("plugin error: {0}")]
Plugin(String),
#[cfg(feature = "validation")]
#[error("validation error: {0}")]
Validation(String),
#[cfg(feature = "plugins")]
#[error("failed to convert plugin output: {0}")]
PluginConversion(String),
}
#[derive(Debug)]
pub struct Ledger {
pub directives: Vec<Spanned<Directive>>,
pub options: Options,
pub plugins: Vec<Plugin>,
pub source_map: SourceMap,
pub errors: Vec<LedgerError>,
pub display_context: DisplayContext,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LedgerError {
pub severity: ErrorSeverity,
pub code: String,
pub message: String,
pub location: Option<ErrorLocation>,
pub source_span: Option<(usize, usize)>,
pub file_id: Option<u16>,
pub phase: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
Error,
Warning,
}
#[derive(Debug, Clone)]
pub struct ErrorLocation {
pub file: std::path::PathBuf,
pub line: usize,
pub column: usize,
}
impl LedgerError {
pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
severity: ErrorSeverity::Error,
code: code.into(),
message: message.into(),
location: None,
source_span: None,
file_id: None,
phase: "validate".to_string(),
}
}
pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
severity: ErrorSeverity::Warning,
code: code.into(),
message: message.into(),
location: None,
source_span: None,
file_id: None,
phase: "validate".to_string(),
}
}
#[must_use]
pub const fn with_source_span(mut self, span: (usize, usize), file_id: u16) -> Self {
self.source_span = Some(span);
self.file_id = Some(file_id);
self
}
#[must_use]
pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
self.phase = phase.into();
self
}
#[must_use]
pub fn with_location(mut self, location: ErrorLocation) -> Self {
self.location = Some(location);
self
}
}
pub fn process(raw: LoadResult, options: &LoadOptions) -> Result<Ledger, ProcessError> {
let mut directives = raw.directives;
let mut errors: Vec<LedgerError> = Vec::new();
for load_err in raw.errors {
errors.push(LedgerError::error("LOAD", load_err.to_string()).with_phase("parse"));
}
directives.sort_by_key(|d| (d.value.date(), d.value.priority(), d.file_id, d.span.start));
#[cfg(feature = "plugins")]
if options.run_plugins || options.auto_accounts {
run_plugins(
&mut directives,
&raw.plugins,
&raw.options,
options,
&raw.source_map,
&mut errors,
PluginPass::PreBookingSynth,
)?;
}
#[cfg(feature = "validation")]
let mut validation_session = if options.validate {
Some(rustledger_validate::ValidationSession::new(
build_validation_options(&raw.options, &raw.source_map),
))
} else {
None
};
#[cfg(feature = "validation")]
let today = jiff::Zoned::now().date();
#[cfg(feature = "validation")]
if let Some(session) = validation_session.as_mut() {
let phase_errors =
session.run_phase_spanned(&directives, rustledger_validate::Phase::Early, today);
ledger_errors_extend(&mut errors, phase_errors, &raw.source_map);
}
#[cfg(feature = "booking")]
let (mut booked, failed): (Vec<Spanned<Directive>>, Vec<Spanned<Directive>>) = {
let file_set_booking = raw.options.set_options.contains("booking_method");
let effective_method = if file_set_booking {
raw.options
.booking_method
.parse()
.unwrap_or(options.booking_method)
} else {
options.booking_method
};
run_booking(directives, effective_method, &mut errors)
};
#[cfg(not(feature = "booking"))]
let (mut booked, failed): (Vec<Spanned<Directive>>, Vec<Spanned<Directive>>) =
(directives, Vec::new());
#[cfg(feature = "plugins")]
if options.run_plugins || !options.extra_plugins.is_empty() {
run_plugins(
&mut booked,
&raw.plugins,
&raw.options,
options,
&raw.source_map,
&mut errors,
PluginPass::PostBooking,
)?;
}
#[cfg(feature = "validation")]
if let Some(mut session) = validation_session {
let phase_errors =
session.run_phase_spanned(&booked, rustledger_validate::Phase::Late, today);
ledger_errors_extend(&mut errors, phase_errors, &raw.source_map);
let finalize_errors = session.finalize();
ledger_errors_extend(&mut errors, finalize_errors, &raw.source_map);
}
let mut directives = booked;
directives.extend(failed);
directives.sort_by_key(|d| (d.value.date(), d.value.priority(), d.file_id, d.span.start));
Ok(Ledger {
directives,
options: raw.options,
plugins: raw.plugins,
source_map: raw.source_map,
errors,
display_context: raw.display_context,
})
}
#[cfg(feature = "booking")]
fn run_booking(
mut directives: Vec<Spanned<Directive>>,
booking_method: BookingMethod,
errors: &mut Vec<LedgerError>,
) -> (Vec<Spanned<Directive>>, Vec<Spanned<Directive>>) {
use rustledger_booking::BookingEngine;
let mut engine = BookingEngine::with_method(booking_method);
engine.register_account_methods(directives.iter().map(|s| &s.value));
let mut order: Vec<usize> = (0..directives.len()).collect();
order.sort_by_key(|&i| {
let d = &directives[i].value;
(d.date(), d.priority(), d.has_cost_reduction())
});
let mut failed_indices: Vec<usize> = Vec::new();
for &i in &order {
let spanned = &mut directives[i];
if let Directive::Transaction(txn) = &mut spanned.value {
match engine.book_and_interpolate(txn) {
Ok(result) => {
engine.apply(&result.transaction);
*txn = result.transaction;
}
Err(e) => {
errors.push(LedgerError::error(
"BOOK",
format!("{} ({}, \"{}\")", e, txn.date, txn.narration),
));
failed_indices.push(i);
}
}
}
}
let failed_set: rustc_hash::FxHashSet<usize> = failed_indices.iter().copied().collect();
let mut booked = Vec::with_capacity(directives.len() - failed_indices.len());
let mut failed = Vec::with_capacity(failed_indices.len());
for (i, d) in directives.into_iter().enumerate() {
if failed_set.contains(&i) {
failed.push(d);
} else {
booked.push(d);
}
}
(booked, failed)
}
#[cfg(feature = "plugins")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginPass {
PreBookingSynth,
PostBooking,
All,
}
#[cfg(feature = "plugins")]
pub fn run_plugins(
directives: &mut Vec<Spanned<Directive>>,
file_plugins: &[Plugin],
file_options: &Options,
options: &LoadOptions,
source_map: &SourceMap,
errors: &mut Vec<LedgerError>,
pass: PluginPass,
) -> Result<(), ProcessError> {
use rustledger_plugin::{
DocumentDiscoveryPlugin, NativePlugin, NativePluginRegistry, PluginInput, PluginOptions,
};
let base_dir = source_map
.files()
.first()
.and_then(|f| f.path.parent())
.unwrap_or_else(|| std::path::Path::new("."));
let run_doc_discovery = matches!(pass, PluginPass::PreBookingSynth | PluginPass::All)
&& options.run_plugins
&& !file_options.documents.is_empty();
let has_document_dirs = run_doc_discovery;
let resolved_documents: Vec<String> = if has_document_dirs {
file_options
.documents
.iter()
.map(|d| {
let path = std::path::Path::new(d);
if path.is_absolute() {
d.clone()
} else {
base_dir.join(path).to_string_lossy().to_string()
}
})
.collect()
} else {
Vec::new()
};
let registry = NativePluginRegistry::new();
let mut raw_plugins: Vec<(String, Option<String>, bool)> = Vec::new();
let is_synth = |name: &str| -> bool { registry.find(name).is_some_and(NativePlugin::is_synth) };
if options.auto_accounts && matches!(pass, PluginPass::PreBookingSynth | PluginPass::All) {
raw_plugins.push(("auto_accounts".to_string(), None, false));
}
if options.run_plugins {
for plugin in file_plugins {
let synth = is_synth(&plugin.name);
let in_pass = match pass {
PluginPass::PreBookingSynth => synth,
PluginPass::PostBooking => !synth,
PluginPass::All => true,
};
if in_pass {
raw_plugins.push((
plugin.name.clone(),
plugin.config.clone(),
plugin.force_python,
));
}
}
}
for (i, plugin_name) in options.extra_plugins.iter().enumerate() {
let synth = is_synth(plugin_name);
let in_pass = match pass {
PluginPass::PreBookingSynth => synth,
PluginPass::PostBooking => !synth,
PluginPass::All => true,
};
if in_pass {
let config = options.extra_plugin_configs.get(i).cloned().flatten();
raw_plugins.push((plugin_name.clone(), config, false));
}
}
if raw_plugins.is_empty() && !has_document_dirs {
return Ok(());
}
let plugin_options = PluginOptions {
operating_currencies: file_options.operating_currency.clone(),
title: file_options.title.clone(),
};
if has_document_dirs {
let doc_plugin = DocumentDiscoveryPlugin::new(resolved_documents, base_dir.to_path_buf());
let wrappers = build_wrappers(directives, source_map);
let input = PluginInput {
directives: wrappers,
options: plugin_options.clone(),
config: None,
};
let output = doc_plugin.process(input);
record_plugin_errors(errors, output.errors, source_map);
apply_plugin_ops(directives, output.ops, errors, source_map)?;
}
if !raw_plugins.is_empty() {
for (raw_name, plugin_config, force_python) in &raw_plugins {
let resolved_name = if *force_python {
None
} else if registry.find(raw_name).is_some() {
Some(raw_name.as_str())
} else if let Some(short_name) = raw_name.strip_prefix("beancount.plugins.") {
registry.find(short_name).is_some().then_some(short_name)
} else if let Some(short_name) = raw_name.strip_prefix("beancount_reds_plugins.") {
registry.find(short_name).is_some().then_some(short_name)
} else if let Some(short_name) = raw_name.strip_prefix("beancount_lazy_plugins.") {
registry.find(short_name).is_some().then_some(short_name)
} else {
None
};
if let Some(name) = resolved_name
&& let Some(plugin) = registry.find(name)
{
let wrappers = build_wrappers(directives, source_map);
let input = PluginInput {
directives: wrappers,
options: plugin_options.clone(),
config: plugin_config.clone(),
};
let output = plugin.process(input);
record_plugin_errors(errors, output.errors, source_map);
apply_plugin_ops(directives, output.ops, errors, source_map)?;
} else {
let plugin_path = std::path::Path::new(raw_name);
let ext = plugin_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
#[cfg_attr(
not(any(feature = "wasm-plugins", feature = "python-plugins")),
allow(unused_variables)
)]
let resolve_path = |name: &str| -> Result<std::path::PathBuf, String> {
let p = std::path::Path::new(name);
let resolved = if p.is_absolute() {
p.to_path_buf()
} else {
base_dir.join(name)
};
if options.path_security
&& let (Ok(canon_base), Ok(canon_plugin)) =
(base_dir.canonicalize(), resolved.canonicalize())
&& !canon_plugin.starts_with(&canon_base)
{
return Err(format!(
"plugin path '{name}' is outside the ledger directory"
));
}
Ok(resolved)
};
if ext == "wasm" {
#[cfg(feature = "wasm-plugins")]
{
let wasm_path = match resolve_path(raw_name) {
Ok(p) => p,
Err(e) => {
errors.push(LedgerError::error("PLUGIN", e).with_phase("plugin"));
continue;
}
};
let wrappers = build_wrappers(directives, source_map);
match run_wasm_plugin(&wasm_path, &wrappers, &plugin_options, plugin_config)
{
Ok((ops, plugin_errors)) => {
for err in plugin_errors {
errors.push(err);
}
apply_plugin_ops(directives, ops, errors, source_map)?;
}
Err(e) => {
errors.push(
LedgerError::error(
"PLUGIN",
format!("WASM plugin {} failed: {e}", wasm_path.display()),
)
.with_phase("plugin"),
);
}
}
}
#[cfg(not(feature = "wasm-plugins"))]
{
errors.push(
LedgerError::error(
"PLUGIN",
format!(
"WASM plugin '{raw_name}' requires the wasm-plugins feature",
),
)
.with_phase("plugin"),
);
}
} else if *force_python
|| ext == "py"
|| raw_name.contains(std::path::MAIN_SEPARATOR)
|| raw_name.contains('.')
{
#[cfg(feature = "python-plugins")]
{
let resolved = match resolve_path(raw_name) {
Ok(p) => p,
Err(e) => {
errors.push(LedgerError::error("PLUGIN", e).with_phase("plugin"));
continue;
}
};
let wrappers = build_wrappers(directives, source_map);
match run_python_plugin(
raw_name,
&resolved,
base_dir,
&wrappers,
&plugin_options,
plugin_config,
) {
Ok((ops, plugin_errors)) => {
for err in plugin_errors {
errors.push(err);
}
apply_plugin_ops(directives, ops, errors, source_map)?;
}
Err(e) => {
errors.push(LedgerError::error("E8002", e).with_phase("plugin"));
}
}
}
#[cfg(not(feature = "python-plugins"))]
{
errors.push(
LedgerError::error(
"E8005",
format!(
"Python plugin \"{raw_name}\" requires the python-plugins feature",
),
)
.with_phase("plugin"),
);
}
} else {
#[cfg(feature = "python-plugins")]
{
use rustledger_plugin::python::{is_python_available, suggest_module_path};
let suggestion = if is_python_available() {
suggest_module_path(raw_name)
} else {
None
};
if let Some(module_path) = suggestion {
errors.push(
LedgerError::error(
"E8004",
format!(
"Cannot resolve Python module '{raw_name}'. Replace with: plugin \"{module_path}\""
),
)
.with_phase("plugin"),
);
} else {
errors.push(
LedgerError::error(
"E8001",
format!("Plugin not found: \"{raw_name}\""),
)
.with_phase("plugin"),
);
}
}
#[cfg(not(feature = "python-plugins"))]
{
errors.push(
LedgerError::error(
"E8001",
format!("Plugin not found: \"{raw_name}\""),
)
.with_phase("plugin"),
);
}
}
}
}
}
Ok(())
}
#[cfg(feature = "plugins")]
fn build_wrappers(
directives: &[Spanned<Directive>],
source_map: &SourceMap,
) -> Vec<rustledger_plugin::DirectiveWrapper> {
use rustledger_plugin::directive_to_wrapper_with_location;
directives
.iter()
.map(|spanned| {
let (filename, lineno) = if let Some(file) = source_map.get(spanned.file_id as usize) {
let (line, _col) = file.line_col(spanned.span.start);
(Some(file.path.display().to_string()), Some(line as u32))
} else {
(None, None)
};
directive_to_wrapper_with_location(&spanned.value, filename, lineno)
})
.collect()
}
#[cfg(feature = "plugins")]
fn record_plugin_errors(
errors: &mut Vec<LedgerError>,
plugin_errors: Vec<rustledger_plugin::PluginError>,
source_map: &SourceMap,
) {
for err in plugin_errors {
let mut ledger_err = match err.severity {
rustledger_plugin::PluginErrorSeverity::Error => {
LedgerError::error("PLUGIN", err.message).with_phase("plugin")
}
rustledger_plugin::PluginErrorSeverity::Warning => {
LedgerError::warning("PLUGIN", err.message).with_phase("plugin")
}
};
if let (Some(file), Some(line)) = (&err.source_file, err.line_number) {
let resolved_path = source_map
.get_by_path(std::path::Path::new(file))
.map_or_else(|| std::path::PathBuf::from(file), |f| f.path.clone());
ledger_err = ledger_err.with_location(ErrorLocation {
file: resolved_path,
line: line as usize,
column: 1,
});
}
errors.push(ledger_err);
}
}
#[cfg(feature = "plugins")]
fn apply_plugin_ops(
directives: &mut Vec<Spanned<Directive>>,
ops: Vec<rustledger_plugin::PluginOp>,
errors: &mut Vec<LedgerError>,
source_map: &SourceMap,
) -> Result<(), ProcessError> {
use rustledger_plugin::PluginOp;
use rustledger_plugin::wrapper_to_directive;
let n = directives.len();
let mut seen = vec![false; n];
for op in &ops {
let idx = match op {
PluginOp::Keep(i) | PluginOp::Modify(i, _) | PluginOp::Delete(i) => Some(*i),
PluginOp::Insert(_) => None,
};
if let Some(i) = idx {
if i >= n {
errors.push(
LedgerError::error(
"PLUGIN",
format!(
"plugin op references out-of-bounds input index {i} (input has {n} directives)"
),
)
.with_phase("plugin"),
);
return Ok(());
}
if seen[i] {
errors.push(
LedgerError::error(
"PLUGIN",
format!("plugin op references input index {i} more than once"),
)
.with_phase("plugin"),
);
return Ok(());
}
seen[i] = true;
}
}
for (i, was_seen) in seen.iter().enumerate() {
if !was_seen {
errors.push(
LedgerError::error(
"PLUGIN",
format!(
"plugin omitted input directive {i} (must appear in exactly one of Keep/Modify/Delete)"
),
)
.with_phase("plugin"),
);
return Ok(());
}
}
let mut new_directives = Vec::with_capacity(ops.len());
for op in ops {
match op {
PluginOp::Keep(i) => {
new_directives.push(directives[i].clone());
}
PluginOp::Modify(i, wrapper) => {
let directive = wrapper_to_directive(&wrapper)
.map_err(|e| ProcessError::PluginConversion(e.to_string()))?;
new_directives.push(Spanned {
value: directive,
span: directives[i].span,
file_id: directives[i].file_id,
});
}
PluginOp::Insert(wrapper) => {
let (span, file_id) = match (&wrapper.filename, wrapper.lineno) {
(Some(filename), Some(lineno)) => {
if let Some(file) = source_map.get_by_path(std::path::Path::new(filename)) {
let span_start = file.line_start(lineno as usize).unwrap_or(0);
(
rustledger_parser::Span::new(span_start, span_start),
file.id as u16,
)
} else {
(
rustledger_parser::Span::new(0, 0),
rustledger_parser::SYNTHESIZED_FILE_ID,
)
}
}
_ => (
rustledger_parser::Span::new(0, 0),
rustledger_parser::SYNTHESIZED_FILE_ID,
),
};
let directive = wrapper_to_directive(&wrapper)
.map_err(|e| ProcessError::PluginConversion(e.to_string()))?;
new_directives.push(Spanned::new(directive, span).with_file_id(file_id as usize));
}
PluginOp::Delete(_) => {}
}
}
*directives = new_directives;
Ok(())
}
#[cfg(feature = "validation")]
fn build_validation_options(
file_options: &Options,
source_map: &SourceMap,
) -> rustledger_validate::ValidationOptions {
use rustledger_validate::ValidationOptions;
let base_dir = source_map
.files()
.first()
.and_then(|f| f.path.parent())
.unwrap_or_else(|| std::path::Path::new("."));
let resolved_document_dirs: Vec<std::path::PathBuf> = file_options
.documents
.iter()
.map(|d| {
let path = std::path::Path::new(d);
if path.is_absolute() {
path.to_path_buf()
} else {
base_dir.join(path)
}
})
.collect();
let account_types: Vec<String> = file_options
.account_types()
.iter()
.map(|s| (*s).to_string())
.collect();
ValidationOptions::default()
.with_account_types(account_types)
.with_document_dirs(resolved_document_dirs)
.with_infer_tolerance_from_cost(file_options.infer_tolerance_from_cost)
.with_tolerance_multiplier(file_options.inferred_tolerance_multiplier)
.with_inferred_tolerance_default(file_options.inferred_tolerance_default.clone())
}
#[cfg(feature = "validation")]
fn ledger_errors_extend(
errors: &mut Vec<LedgerError>,
validation_errors: Vec<rustledger_validate::ValidationError>,
source_map: &SourceMap,
) {
for err in validation_errors {
let phase = if err.code.is_parse_phase() {
"parse"
} else {
"validate"
};
let severity_level = if err.code.is_warning() {
ErrorSeverity::Warning
} else {
ErrorSeverity::Error
};
let message = match &err.note {
Some(note) => format!("{err}\n note: {note}"),
None => err.to_string(),
};
let location = err.span.and_then(|span| {
let fid = err.file_id? as usize;
let file = source_map.get(fid)?;
let (line, column) = file.line_col(span.start);
Some(ErrorLocation {
file: file.path.clone(),
line,
column,
})
});
errors.push(LedgerError {
severity: severity_level,
code: err.code.code().to_string(),
message,
location,
source_span: err.span.map(|s| (s.start, s.end)),
file_id: err.file_id,
phase: phase.to_string(),
});
}
}
pub fn load(path: &Path, options: &LoadOptions) -> Result<Ledger, ProcessError> {
let mut loader = crate::Loader::new();
if options.path_security {
loader = loader.with_path_security(true);
}
let raw = loader.load(path)?;
process(raw, options)
}
pub fn load_raw(path: &Path) -> Result<LoadResult, LoadError> {
crate::Loader::new().load(path)
}
#[cfg(feature = "wasm-plugins")]
fn run_wasm_plugin(
wasm_path: &std::path::Path,
directives: &[rustledger_plugin::DirectiveWrapper],
options: &rustledger_plugin::PluginOptions,
config: &Option<String>,
) -> Result<(Vec<rustledger_plugin::PluginOp>, Vec<LedgerError>), String> {
use rustledger_plugin::{PluginInput, PluginManager};
let mut mgr = PluginManager::new();
let plugin_idx = mgr
.load(wasm_path)
.map_err(|e| format!("failed to load: {e}"))?;
let input = PluginInput {
directives: directives.to_vec(),
options: options.clone(),
config: config.clone(),
};
let output = mgr
.execute(plugin_idx, &input)
.map_err(|e| format!("execution failed: {e}"))?;
let mut errors = Vec::new();
for err in output.errors {
let ledger_err = match err.severity {
rustledger_plugin::PluginErrorSeverity::Error => {
LedgerError::error("PLUGIN", err.message).with_phase("plugin")
}
rustledger_plugin::PluginErrorSeverity::Warning => {
LedgerError::warning("PLUGIN", err.message).with_phase("plugin")
}
};
errors.push(ledger_err);
}
Ok((output.ops, errors))
}
#[cfg(feature = "python-plugins")]
fn run_python_plugin(
module_name: &str,
resolved_path: &std::path::Path,
base_dir: &std::path::Path,
directives: &[rustledger_plugin::DirectiveWrapper],
options: &rustledger_plugin::PluginOptions,
config: &Option<String>,
) -> Result<(Vec<rustledger_plugin::PluginOp>, Vec<LedgerError>), String> {
use rustledger_plugin::{PluginInput, python::PythonRuntime};
let runtime = PythonRuntime::new().map_err(|e| format!("Python runtime unavailable: {e}"))?;
let input = PluginInput {
directives: directives.to_vec(),
options: options.clone(),
config: config.clone(),
};
let is_file = resolved_path.exists()
|| std::path::Path::new(module_name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("py"))
|| module_name.contains(std::path::MAIN_SEPARATOR);
let output = if is_file {
runtime
.execute_module(module_name, &input, Some(base_dir))
.map_err(|e| format!("Python plugin execution failed: {e}"))?
} else {
runtime
.execute_module(module_name, &input, Some(base_dir))
.map_err(|e| format!("Python plugin '{module_name}' execution failed: {e}"))?
};
let mut errors = Vec::new();
for err in output.errors {
let ledger_err = match err.severity {
rustledger_plugin::PluginErrorSeverity::Error => {
LedgerError::error("PLUGIN", err.message).with_phase("plugin")
}
rustledger_plugin::PluginErrorSeverity::Warning => {
LedgerError::warning("PLUGIN", err.message).with_phase("plugin")
}
};
errors.push(ledger_err);
}
Ok((output.ops, errors))
}