use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInput {
pub directives: Vec<DirectiveWrapper>,
pub options: PluginOptions,
pub config: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginOutput {
pub directives: Vec<DirectiveWrapper>,
pub errors: Vec<PluginError>,
}
impl PluginOutput {
#[must_use]
pub const fn passthrough(directives: Vec<DirectiveWrapper>) -> Self {
Self {
directives,
errors: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginOptions {
pub operating_currencies: Vec<String>,
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginError {
pub message: String,
pub source_file: Option<String>,
pub line_number: Option<u32>,
pub severity: PluginErrorSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginErrorSeverity {
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
}
impl PluginError {
#[must_use]
pub fn error(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source_file: None,
line_number: None,
severity: PluginErrorSeverity::Error,
}
}
#[must_use]
pub fn warning(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source_file: None,
line_number: None,
severity: PluginErrorSeverity::Warning,
}
}
#[must_use]
pub fn at(mut self, file: impl Into<String>, line: u32) -> Self {
self.source_file = Some(file.into());
self.line_number = Some(line);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectiveWrapper {
#[serde(skip_serializing, default)]
pub directive_type: String,
pub date: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub lineno: Option<u32>,
#[serde(flatten)]
pub data: DirectiveData,
}
impl DirectiveWrapper {
#[must_use]
pub const fn type_sort_order(&self) -> i8 {
match &self.data {
DirectiveData::Open(_) => -2,
DirectiveData::Balance(_) => -1,
DirectiveData::Document(_) => 1,
DirectiveData::Close(_) => 2,
_ => 0,
}
}
#[must_use]
pub fn sort_key(&self) -> (&str, i8, u32) {
(
&self.date,
self.type_sort_order(),
self.lineno.unwrap_or(u32::MAX),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DirectiveData {
#[serde(rename = "transaction")]
Transaction(TransactionData),
#[serde(rename = "balance")]
Balance(BalanceData),
#[serde(rename = "open")]
Open(OpenData),
#[serde(rename = "close")]
Close(CloseData),
#[serde(rename = "commodity")]
Commodity(CommodityData),
#[serde(rename = "pad")]
Pad(PadData),
#[serde(rename = "event")]
Event(EventData),
#[serde(rename = "note")]
Note(NoteData),
#[serde(rename = "document")]
Document(DocumentData),
#[serde(rename = "price")]
Price(PriceData),
#[serde(rename = "query")]
Query(QueryData),
#[serde(rename = "custom")]
Custom(CustomData),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionData {
pub flag: String,
pub payee: Option<String>,
pub narration: String,
pub tags: Vec<String>,
pub links: Vec<String>,
pub metadata: Vec<(String, MetaValueData)>,
pub postings: Vec<PostingData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostingData {
pub account: String,
pub units: Option<AmountData>,
pub cost: Option<CostData>,
pub price: Option<PriceAnnotationData>,
pub flag: Option<String>,
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmountData {
pub number: String,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostData {
pub number_per: Option<String>,
pub number_total: Option<String>,
pub currency: Option<String>,
pub date: Option<String>,
pub label: Option<String>,
pub merge: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceAnnotationData {
pub is_total: bool,
pub amount: Option<AmountData>,
pub number: Option<String>,
pub currency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum MetaValueData {
#[serde(rename = "string")]
String(String),
#[serde(rename = "number")]
Number(String),
#[serde(rename = "date")]
Date(String),
#[serde(rename = "account")]
Account(String),
#[serde(rename = "currency")]
Currency(String),
#[serde(rename = "tag")]
Tag(String),
#[serde(rename = "link")]
Link(String),
#[serde(rename = "amount")]
Amount(AmountData),
#[serde(rename = "bool")]
Bool(bool),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceData {
pub account: String,
pub amount: AmountData,
pub tolerance: Option<String>,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenData {
pub account: String,
pub currencies: Vec<String>,
pub booking: Option<String>,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseData {
pub account: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommodityData {
pub currency: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PadData {
pub account: String,
pub source_account: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventData {
pub event_type: String,
pub value: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteData {
pub account: String,
pub comment: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentData {
pub account: String,
pub path: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceData {
pub currency: String,
pub amount: AmountData,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryData {
pub name: String,
pub query: String,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomData {
pub custom_type: String,
pub values: Vec<MetaValueData>,
#[serde(default)]
pub metadata: Vec<(String, MetaValueData)>,
}
pub fn sort_directives(directives: &mut [DirectiveWrapper]) {
directives.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_error_builder() {
let error = PluginError::error("test error").at("file.beancount", 10);
assert_eq!(error.message, "test error");
assert_eq!(error.source_file, Some("file.beancount".to_string()));
assert_eq!(error.line_number, Some(10));
assert_eq!(error.severity, PluginErrorSeverity::Error);
}
#[test]
fn test_plugin_warning() {
let warning = PluginError::warning("test warning");
assert_eq!(warning.severity, PluginErrorSeverity::Warning);
}
#[test]
fn test_directive_sort_order() {
let open = DirectiveWrapper {
directive_type: String::new(),
date: "2024-01-01".to_string(),
filename: None,
lineno: Some(1),
data: DirectiveData::Open(OpenData {
account: "Assets:Bank".to_string(),
currencies: vec![],
booking: None,
metadata: vec![],
}),
};
assert_eq!(open.type_sort_order(), -2);
let close = DirectiveWrapper {
directive_type: String::new(),
date: "2024-01-01".to_string(),
filename: None,
lineno: Some(2),
data: DirectiveData::Close(CloseData {
account: "Assets:Bank".to_string(),
metadata: vec![],
}),
};
assert_eq!(close.type_sort_order(), 2);
}
#[test]
fn test_serde_roundtrip() {
let input = PluginInput {
directives: vec![DirectiveWrapper {
directive_type: String::new(),
date: "2024-01-15".to_string(),
filename: Some("test.beancount".to_string()),
lineno: Some(42),
data: DirectiveData::Transaction(TransactionData {
flag: "*".to_string(),
payee: Some("Coffee Shop".to_string()),
narration: "Morning coffee".to_string(),
tags: vec!["food".to_string()],
links: vec![],
metadata: vec![],
postings: vec![PostingData {
account: "Expenses:Food".to_string(),
units: Some(AmountData {
number: "5.00".to_string(),
currency: "USD".to_string(),
}),
cost: None,
price: None,
flag: None,
metadata: vec![],
}],
}),
}],
options: PluginOptions {
operating_currencies: vec!["USD".to_string()],
title: Some("Test Ledger".to_string()),
},
config: Some("threshold=100".to_string()),
};
let json = serde_json::to_string(&input).unwrap();
let decoded: PluginInput = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.directives.len(), 1);
assert_eq!(decoded.config, Some("threshold=100".to_string()));
let msgpack = rmp_serde::to_vec(&input).unwrap();
let decoded: PluginInput = rmp_serde::from_slice(&msgpack).unwrap();
assert_eq!(decoded.directives.len(), 1);
}
}