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 pub phase: String,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum ErrorSeverity {
137 Error,
139 Warning,
141}
142
143#[derive(Debug, Clone)]
145pub struct ErrorLocation {
146 pub file: std::path::PathBuf,
148 pub line: usize,
150 pub column: usize,
152}
153
154impl LedgerError {
155 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
157 Self {
158 severity: ErrorSeverity::Error,
159 code: code.into(),
160 message: message.into(),
161 location: None,
162 phase: "validate".to_string(),
163 }
164 }
165
166 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
168 Self {
169 severity: ErrorSeverity::Warning,
170 code: code.into(),
171 message: message.into(),
172 location: None,
173 phase: "validate".to_string(),
174 }
175 }
176
177 #[must_use]
179 pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
180 self.phase = phase.into();
181 self
182 }
183
184 #[must_use]
186 pub fn with_location(mut self, location: ErrorLocation) -> Self {
187 self.location = Some(location);
188 self
189 }
190}
191
192pub fn process(raw: LoadResult, options: &LoadOptions) -> Result<Ledger, ProcessError> {
200 let mut directives = raw.directives;
201 let mut errors: Vec<LedgerError> = Vec::new();
202
203 for load_err in raw.errors {
205 errors.push(LedgerError::error("LOAD", load_err.to_string()).with_phase("parse"));
206 }
207
208 directives.sort_by(|a, b| {
210 a.value
211 .date()
212 .cmp(&b.value.date())
213 .then_with(|| a.value.priority().cmp(&b.value.priority()))
214 });
215
216 #[cfg(feature = "booking")]
230 {
231 let file_set_booking = raw.options.set_options.contains("booking_method");
232 let effective_method = if file_set_booking {
233 raw.options
234 .booking_method
235 .parse()
236 .unwrap_or(options.booking_method)
237 } else {
238 options.booking_method
239 };
240 run_booking(&mut directives, effective_method, &mut errors);
241 }
242
243 #[cfg(feature = "plugins")]
247 if options.run_plugins || !options.extra_plugins.is_empty() || options.auto_accounts {
248 run_plugins(
249 &mut directives,
250 &raw.plugins,
251 &raw.options,
252 options,
253 &raw.source_map,
254 &mut errors,
255 )?;
256 }
257
258 #[cfg(feature = "validation")]
260 if options.validate {
261 run_validation(&directives, &raw.options, &mut errors);
262 }
263
264 Ok(Ledger {
265 directives,
266 options: raw.options,
267 plugins: raw.plugins,
268 source_map: raw.source_map,
269 errors,
270 display_context: raw.display_context,
271 })
272}
273
274#[cfg(feature = "booking")]
276fn run_booking(
277 directives: &mut Vec<Spanned<Directive>>,
278 booking_method: BookingMethod,
279 errors: &mut Vec<LedgerError>,
280) {
281 use rustledger_booking::BookingEngine;
282
283 let mut engine = BookingEngine::with_method(booking_method);
284 engine.register_account_methods(directives.iter().map(|s| &s.value));
285
286 for spanned in directives.iter_mut() {
287 if let Directive::Transaction(txn) = &mut spanned.value {
288 match engine.book_and_interpolate(txn) {
289 Ok(result) => {
290 engine.apply(&result.transaction);
291 *txn = result.transaction;
292 }
293 Err(e) => {
294 errors.push(LedgerError::error(
295 "BOOK",
296 format!("{} ({}, \"{}\")", e, txn.date, txn.narration),
297 ));
298 }
299 }
300 }
301 }
302}
303
304#[cfg(feature = "plugins")]
313pub fn run_plugins(
314 directives: &mut Vec<Spanned<Directive>>,
315 file_plugins: &[Plugin],
316 file_options: &Options,
317 options: &LoadOptions,
318 source_map: &SourceMap,
319 errors: &mut Vec<LedgerError>,
320) -> Result<(), ProcessError> {
321 use rustledger_plugin::{
322 DocumentDiscoveryPlugin, NativePlugin, NativePluginRegistry, PluginInput, PluginOptions,
323 directive_to_wrapper_with_location, wrapper_to_directive,
324 };
325
326 let base_dir = source_map
329 .files()
330 .first()
331 .and_then(|f| f.path.parent())
332 .unwrap_or_else(|| std::path::Path::new("."));
333
334 let has_document_dirs = options.run_plugins && !file_options.documents.is_empty();
335 let resolved_documents: Vec<String> = if has_document_dirs {
336 file_options
337 .documents
338 .iter()
339 .map(|d| {
340 let path = std::path::Path::new(d);
341 if path.is_absolute() {
342 d.clone()
343 } else {
344 base_dir.join(path).to_string_lossy().to_string()
345 }
346 })
347 .collect()
348 } else {
349 Vec::new()
350 };
351
352 let mut raw_plugins: Vec<(String, Option<String>)> = Vec::new();
354
355 if options.auto_accounts {
357 raw_plugins.push(("auto_accounts".to_string(), None));
358 }
359
360 if options.run_plugins {
362 for plugin in file_plugins {
363 raw_plugins.push((plugin.name.clone(), plugin.config.clone()));
364 }
365 }
366
367 for (i, plugin_name) in options.extra_plugins.iter().enumerate() {
369 let config = options.extra_plugin_configs.get(i).cloned().flatten();
370 raw_plugins.push((plugin_name.clone(), config));
371 }
372
373 if raw_plugins.is_empty() && !has_document_dirs {
375 return Ok(());
376 }
377
378 let mut wrappers: Vec<_> = directives
380 .iter()
381 .map(|spanned| {
382 let (filename, lineno) = if let Some(file) = source_map.get(spanned.file_id as usize) {
383 let (line, _col) = file.line_col(spanned.span.start);
384 (Some(file.path.display().to_string()), Some(line as u32))
385 } else {
386 (None, None)
387 };
388 directive_to_wrapper_with_location(&spanned.value, filename, lineno)
389 })
390 .collect();
391
392 let plugin_options = PluginOptions {
393 operating_currencies: file_options.operating_currency.clone(),
394 title: file_options.title.clone(),
395 };
396
397 if has_document_dirs {
399 let doc_plugin = DocumentDiscoveryPlugin::new(resolved_documents, base_dir.to_path_buf());
400 let input = PluginInput {
401 directives: wrappers.clone(),
402 options: plugin_options.clone(),
403 config: None,
404 };
405 let output = doc_plugin.process(input);
406
407 for err in output.errors {
409 let ledger_err = match err.severity {
410 rustledger_plugin::PluginErrorSeverity::Error => {
411 LedgerError::error("PLUGIN", err.message)
412 }
413 rustledger_plugin::PluginErrorSeverity::Warning => {
414 LedgerError::warning("PLUGIN", err.message)
415 }
416 };
417 errors.push(ledger_err);
418 }
419
420 wrappers = output.directives;
421 }
422
423 if !raw_plugins.is_empty() {
425 let registry = NativePluginRegistry::new();
426
427 for (raw_name, plugin_config) in &raw_plugins {
428 let resolved_name = if registry.find(raw_name).is_some() {
430 Some(raw_name.as_str())
431 } else if let Some(short_name) = raw_name.strip_prefix("beancount.plugins.") {
432 registry.find(short_name).is_some().then_some(short_name)
433 } else if let Some(short_name) = raw_name.strip_prefix("beancount_reds_plugins.") {
434 registry.find(short_name).is_some().then_some(short_name)
435 } else if let Some(short_name) = raw_name.strip_prefix("beancount_lazy_plugins.") {
436 registry.find(short_name).is_some().then_some(short_name)
437 } else {
438 None
439 };
440
441 if let Some(name) = resolved_name
442 && let Some(plugin) = registry.find(name)
443 {
444 let input = PluginInput {
445 directives: wrappers.clone(),
446 options: plugin_options.clone(),
447 config: plugin_config.clone(),
448 };
449
450 let output = plugin.process(input);
451
452 for err in output.errors {
454 let ledger_err = match err.severity {
455 rustledger_plugin::PluginErrorSeverity::Error => {
456 LedgerError::error("PLUGIN", err.message).with_phase("plugin")
457 }
458 rustledger_plugin::PluginErrorSeverity::Warning => {
459 LedgerError::warning("PLUGIN", err.message).with_phase("plugin")
460 }
461 };
462 errors.push(ledger_err);
463 }
464
465 wrappers = output.directives;
466 }
467 }
468 }
469
470 let filename_to_file_id: std::collections::HashMap<String, u16> = source_map
472 .files()
473 .iter()
474 .map(|f| (f.path.display().to_string(), f.id as u16))
475 .collect();
476
477 let mut new_directives = Vec::with_capacity(wrappers.len());
479 for wrapper in &wrappers {
480 let directive = wrapper_to_directive(wrapper)
481 .map_err(|e| ProcessError::PluginConversion(e.to_string()))?;
482
483 let (span, file_id) =
485 if let (Some(filename), Some(lineno)) = (&wrapper.filename, wrapper.lineno) {
486 if let Some(&fid) = filename_to_file_id.get(filename) {
487 if let Some(file) = source_map.get(fid as usize) {
489 let span_start = file.line_start(lineno as usize).unwrap_or(0);
490 (rustledger_parser::Span::new(span_start, span_start), fid)
491 } else {
492 (rustledger_parser::Span::new(0, 0), 0)
493 }
494 } else {
495 (rustledger_parser::Span::new(0, 0), 0)
497 }
498 } else {
499 (rustledger_parser::Span::new(0, 0), 0)
501 };
502
503 new_directives.push(Spanned::new(directive, span).with_file_id(file_id as usize));
504 }
505
506 *directives = new_directives;
507 Ok(())
508}
509
510#[cfg(feature = "validation")]
512fn run_validation(
513 directives: &[Spanned<Directive>],
514 file_options: &Options,
515 errors: &mut Vec<LedgerError>,
516) {
517 use rustledger_validate::{ValidationOptions, validate_spanned_with_options};
518
519 let account_types: Vec<String> = file_options
520 .account_types()
521 .iter()
522 .map(|s| (*s).to_string())
523 .collect();
524
525 let validation_options = ValidationOptions {
526 account_types,
527 infer_tolerance_from_cost: file_options.infer_tolerance_from_cost,
528 tolerance_multiplier: file_options.inferred_tolerance_multiplier,
529 inferred_tolerance_default: file_options.inferred_tolerance_default.clone(),
530 ..Default::default()
531 };
532
533 let validation_errors = validate_spanned_with_options(directives, validation_options);
534
535 for err in validation_errors {
536 let phase = if err.code.is_parse_phase() {
537 "parse"
538 } else {
539 "validate"
540 };
541 let severity_level = if err.code.is_warning() {
542 ErrorSeverity::Warning
543 } else {
544 ErrorSeverity::Error
545 };
546 errors.push(LedgerError {
547 severity: severity_level,
548 code: err.code.code().to_string(),
549 message: err.to_string(),
550 location: None,
551 phase: phase.to_string(),
552 });
553 }
554}
555
556pub fn load(path: &Path, options: &LoadOptions) -> Result<Ledger, ProcessError> {
573 let mut loader = crate::Loader::new();
574
575 if options.path_security {
576 loader = loader.with_path_security(true);
577 }
578
579 let raw = loader.load(path)?;
580 process(raw, options)
581}
582
583pub fn load_raw(path: &Path) -> Result<LoadResult, LoadError> {
588 crate::Loader::new().load(path)
589}