1use crate::{LoadError, LoadResult, Options, Plugin, SourceMap};
7use rustledger_core::{BookingMethod, Directive, DisplayContext};
8use rustledger_parser::Spanned;
9use std::path::Path;
10use thiserror::Error;
11
12#[derive(Debug, Clone)]
14pub struct LoadOptions {
15 pub booking_method: BookingMethod,
17 pub run_plugins: bool,
19 pub auto_accounts: bool,
21 pub extra_plugins: Vec<String>,
23 pub extra_plugin_configs: Vec<Option<String>>,
25 pub validate: bool,
27 pub path_security: bool,
29}
30
31impl Default for LoadOptions {
32 fn default() -> Self {
33 Self {
34 booking_method: BookingMethod::Strict,
35 run_plugins: true,
36 auto_accounts: false,
37 extra_plugins: Vec::new(),
38 extra_plugin_configs: Vec::new(),
39 validate: true,
40 path_security: false,
41 }
42 }
43}
44
45impl LoadOptions {
46 #[must_use]
48 pub const fn raw() -> Self {
49 Self {
50 booking_method: BookingMethod::Strict,
51 run_plugins: false,
52 auto_accounts: false,
53 extra_plugins: Vec::new(),
54 extra_plugin_configs: Vec::new(),
55 validate: false,
56 path_security: false,
57 }
58 }
59}
60
61#[derive(Debug, Error)]
63pub enum ProcessError {
64 #[error("loading failed: {0}")]
66 Load(#[from] LoadError),
67
68 #[cfg(feature = "booking")]
70 #[error("booking error: {message}")]
71 Booking {
72 message: String,
74 date: chrono::NaiveDate,
76 narration: String,
78 },
79
80 #[cfg(feature = "plugins")]
82 #[error("plugin error: {0}")]
83 Plugin(String),
84
85 #[cfg(feature = "validation")]
87 #[error("validation error: {0}")]
88 Validation(String),
89
90 #[cfg(feature = "plugins")]
92 #[error("failed to convert plugin output: {0}")]
93 PluginConversion(String),
94}
95
96#[derive(Debug)]
101pub struct Ledger {
102 pub directives: Vec<Spanned<Directive>>,
104 pub options: Options,
106 pub plugins: Vec<Plugin>,
108 pub source_map: SourceMap,
110 pub errors: Vec<LedgerError>,
112 pub display_context: DisplayContext,
114}
115
116#[derive(Debug)]
121pub struct LedgerError {
122 pub severity: ErrorSeverity,
124 pub code: String,
126 pub message: String,
128 pub location: Option<ErrorLocation>,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ErrorSeverity {
135 Error,
137 Warning,
139}
140
141#[derive(Debug, Clone)]
143pub struct ErrorLocation {
144 pub file: std::path::PathBuf,
146 pub line: usize,
148 pub column: usize,
150}
151
152impl LedgerError {
153 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
155 Self {
156 severity: ErrorSeverity::Error,
157 code: code.into(),
158 message: message.into(),
159 location: None,
160 }
161 }
162
163 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
165 Self {
166 severity: ErrorSeverity::Warning,
167 code: code.into(),
168 message: message.into(),
169 location: None,
170 }
171 }
172
173 #[must_use]
175 pub fn with_location(mut self, location: ErrorLocation) -> Self {
176 self.location = Some(location);
177 self
178 }
179}
180
181pub fn process(raw: LoadResult, options: &LoadOptions) -> Result<Ledger, ProcessError> {
189 let mut directives = raw.directives;
190 let mut errors: Vec<LedgerError> = Vec::new();
191
192 for load_err in raw.errors {
194 errors.push(LedgerError::error("LOAD", load_err.to_string()));
195 }
196
197 directives.sort_by(|a, b| {
199 a.value
200 .date()
201 .cmp(&b.value.date())
202 .then_with(|| a.value.priority().cmp(&b.value.priority()))
203 });
204
205 #[cfg(feature = "booking")]
207 {
208 run_booking(&mut directives, options, &mut errors);
209 }
210
211 #[cfg(feature = "plugins")]
213 if options.run_plugins || !options.extra_plugins.is_empty() || options.auto_accounts {
214 run_plugins(
215 &mut directives,
216 &raw.plugins,
217 &raw.options,
218 options,
219 &mut errors,
220 )?;
221 }
222
223 #[cfg(feature = "validation")]
225 if options.validate {
226 run_validation(&directives, &raw.options, &mut errors);
227 }
228
229 Ok(Ledger {
230 directives,
231 options: raw.options,
232 plugins: raw.plugins,
233 source_map: raw.source_map,
234 errors,
235 display_context: raw.display_context,
236 })
237}
238
239#[cfg(feature = "booking")]
241fn run_booking(
242 directives: &mut Vec<Spanned<Directive>>,
243 options: &LoadOptions,
244 errors: &mut Vec<LedgerError>,
245) {
246 use rustledger_booking::BookingEngine;
247
248 let mut engine = BookingEngine::with_method(options.booking_method);
249
250 for spanned in directives.iter_mut() {
251 if let Directive::Transaction(txn) = &mut spanned.value {
252 match engine.book_and_interpolate(txn) {
253 Ok(result) => {
254 engine.apply(&result.transaction);
255 *txn = result.transaction;
256 }
257 Err(e) => {
258 errors.push(LedgerError::error(
259 "BOOK",
260 format!("{} ({}, \"{}\")", e, txn.date, txn.narration),
261 ));
262 }
263 }
264 }
265 }
266}
267
268#[cfg(feature = "plugins")]
270fn run_plugins(
271 directives: &mut Vec<Spanned<Directive>>,
272 file_plugins: &[Plugin],
273 file_options: &Options,
274 options: &LoadOptions,
275 errors: &mut Vec<LedgerError>,
276) -> Result<(), ProcessError> {
277 use rustledger_plugin::{
278 NativePluginRegistry, PluginInput, PluginOptions, directives_to_wrappers,
279 wrappers_to_directives,
280 };
281
282 let registry = NativePluginRegistry::new();
283
284 let mut plugins_to_run: Vec<(String, Option<String>)> = Vec::new();
286
287 if options.auto_accounts {
289 plugins_to_run.push(("auto_accounts".to_string(), None));
290 }
291
292 if options.run_plugins {
294 for plugin in file_plugins {
295 let plugin_name = if registry.find(&plugin.name).is_some() {
297 plugin.name.clone()
298 } else if let Some(short_name) = plugin.name.strip_prefix("beancount.plugins.") {
299 if registry.find(short_name).is_some() {
300 short_name.to_string()
301 } else {
302 continue;
304 }
305 } else if let Some(short_name) = plugin.name.strip_prefix("beancount_reds_plugins.") {
306 if registry.find(short_name).is_some() {
307 short_name.to_string()
308 } else {
309 continue;
310 }
311 } else if let Some(short_name) = plugin.name.strip_prefix("beancount_lazy_plugins.") {
312 if registry.find(short_name).is_some() {
313 short_name.to_string()
314 } else {
315 continue;
316 }
317 } else {
318 continue;
319 };
320
321 plugins_to_run.push((plugin_name, plugin.config.clone()));
322 }
323 }
324
325 for (i, plugin_name) in options.extra_plugins.iter().enumerate() {
327 let config = options.extra_plugin_configs.get(i).cloned().flatten();
328 plugins_to_run.push((plugin_name.clone(), config));
329 }
330
331 if plugins_to_run.is_empty() {
332 return Ok(());
333 }
334
335 let plain_directives: Vec<Directive> = directives.iter().map(|s| s.value.clone()).collect();
337 let mut wrappers = directives_to_wrappers(&plain_directives);
338
339 let plugin_options = PluginOptions {
340 operating_currencies: file_options.operating_currency.clone(),
341 title: file_options.title.clone(),
342 };
343
344 for (plugin_name, plugin_config) in &plugins_to_run {
346 if let Some(plugin) = registry.find(plugin_name) {
347 let input = PluginInput {
348 directives: wrappers.clone(),
349 options: plugin_options.clone(),
350 config: plugin_config.clone(),
351 };
352
353 let output = plugin.process(input);
354
355 for err in output.errors {
357 let ledger_err = match err.severity {
358 rustledger_plugin::PluginErrorSeverity::Error => {
359 LedgerError::error("PLUGIN", err.message)
360 }
361 rustledger_plugin::PluginErrorSeverity::Warning => {
362 LedgerError::warning("PLUGIN", err.message)
363 }
364 };
365 errors.push(ledger_err);
366 }
367
368 wrappers = output.directives;
369 }
370 }
371
372 let processed = wrappers_to_directives(&wrappers)
374 .map_err(|e| ProcessError::PluginConversion(e.to_string()))?;
375
376 if processed.len() == directives.len() {
378 for (i, new_directive) in processed.into_iter().enumerate() {
380 directives[i].value = new_directive;
381 }
382 } else {
383 *directives = processed
388 .into_iter()
389 .map(|d| Spanned::new(d, rustledger_parser::Span::new(0, 0)))
390 .collect();
391 }
392
393 Ok(())
394}
395
396#[cfg(feature = "validation")]
398fn run_validation(
399 directives: &[Spanned<Directive>],
400 file_options: &Options,
401 errors: &mut Vec<LedgerError>,
402) {
403 use rustledger_validate::{ValidationOptions, validate_spanned_with_options};
404
405 let account_types: Vec<String> = file_options
406 .account_types()
407 .iter()
408 .map(|s| (*s).to_string())
409 .collect();
410
411 let validation_options = ValidationOptions {
412 account_types,
413 infer_tolerance_from_cost: file_options.infer_tolerance_from_cost,
414 tolerance_multiplier: file_options.inferred_tolerance_multiplier,
415 inferred_tolerance_default: file_options.inferred_tolerance_default.clone(),
416 ..Default::default()
417 };
418
419 let validation_errors = validate_spanned_with_options(directives, validation_options);
420
421 for err in validation_errors {
422 errors.push(LedgerError::error(err.code.code(), err.to_string()));
423 }
424}
425
426pub fn load(path: &Path, options: &LoadOptions) -> Result<Ledger, ProcessError> {
443 let mut loader = crate::Loader::new();
444
445 if options.path_security {
446 loader = loader.with_path_security(true);
447 }
448
449 let raw = loader.load(path)?;
450 process(raw, options)
451}
452
453pub fn load_raw(path: &Path) -> Result<LoadResult, LoadError> {
458 crate::Loader::new().load(path)
459}