Skip to main content

rustledger_plugin/
types.rs

1//! Plugin interface types.
2//!
3//! These types define the contract between the plugin host and plugins.
4//! They are serialized via `MessagePack` across the WASM boundary.
5
6use serde::{Deserialize, Serialize};
7
8/// Input passed to a plugin.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PluginInput {
11    /// All directives to process.
12    pub directives: Vec<DirectiveWrapper>,
13    /// Ledger options.
14    pub options: PluginOptions,
15    /// Plugin-specific configuration string.
16    pub config: Option<String>,
17}
18
19/// Output returned from a plugin.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PluginOutput {
22    /// Processed directives (may be modified, added, or removed).
23    pub directives: Vec<DirectiveWrapper>,
24    /// Errors generated by the plugin.
25    pub errors: Vec<PluginError>,
26}
27
28/// A wrapper around directives for serialization.
29///
30/// This wrapper exists because `Directive` contains types that need
31/// special serialization handling (like `Decimal` and `NaiveDate`).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DirectiveWrapper {
34    /// The type of directive (derived from data, not serialized to avoid duplicate keys).
35    #[serde(skip_serializing, default)]
36    pub directive_type: String,
37    /// The directive date (YYYY-MM-DD).
38    pub date: String,
39    /// Source filename (for tracking through plugin processing).
40    /// If None, the directive was created by a plugin.
41    #[serde(skip_serializing_if = "Option::is_none", default)]
42    pub filename: Option<String>,
43    /// Source line number (1-based).
44    /// If None, the directive was created by a plugin.
45    #[serde(skip_serializing_if = "Option::is_none", default)]
46    pub lineno: Option<u32>,
47    /// Directive-specific data as a nested structure.
48    #[serde(flatten)]
49    pub data: DirectiveData,
50}
51
52/// Directive-specific data.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(tag = "type")]
55pub enum DirectiveData {
56    /// Transaction data.
57    #[serde(rename = "transaction")]
58    Transaction(TransactionData),
59    /// Balance assertion data.
60    #[serde(rename = "balance")]
61    Balance(BalanceData),
62    /// Open account data.
63    #[serde(rename = "open")]
64    Open(OpenData),
65    /// Close account data.
66    #[serde(rename = "close")]
67    Close(CloseData),
68    /// Commodity declaration data.
69    #[serde(rename = "commodity")]
70    Commodity(CommodityData),
71    /// Pad directive data.
72    #[serde(rename = "pad")]
73    Pad(PadData),
74    /// Event data.
75    #[serde(rename = "event")]
76    Event(EventData),
77    /// Note data.
78    #[serde(rename = "note")]
79    Note(NoteData),
80    /// Document data.
81    #[serde(rename = "document")]
82    Document(DocumentData),
83    /// Price data.
84    #[serde(rename = "price")]
85    Price(PriceData),
86    /// Query data.
87    #[serde(rename = "query")]
88    Query(QueryData),
89    /// Custom directive data.
90    #[serde(rename = "custom")]
91    Custom(CustomData),
92}
93
94/// Transaction data for serialization.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct TransactionData {
97    /// Transaction flag (* or !).
98    pub flag: String,
99    /// Optional payee.
100    pub payee: Option<String>,
101    /// Narration/description.
102    pub narration: String,
103    /// Tags without the # prefix.
104    pub tags: Vec<String>,
105    /// Links without the ^ prefix.
106    pub links: Vec<String>,
107    /// Metadata key-value pairs.
108    pub metadata: Vec<(String, MetaValueData)>,
109    /// Postings.
110    pub postings: Vec<PostingData>,
111}
112
113/// Posting data for serialization.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct PostingData {
116    /// Account name.
117    pub account: String,
118    /// Units (amount + currency).
119    pub units: Option<AmountData>,
120    /// Cost specification.
121    pub cost: Option<CostData>,
122    /// Price annotation.
123    pub price: Option<PriceAnnotationData>,
124    /// Optional posting flag.
125    pub flag: Option<String>,
126    /// Posting metadata.
127    pub metadata: Vec<(String, MetaValueData)>,
128}
129
130/// Amount data for serialization.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AmountData {
133    /// Number as string (preserves precision).
134    pub number: String,
135    /// Currency code.
136    pub currency: String,
137}
138
139/// Cost data for serialization.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct CostData {
142    /// Per-unit cost number.
143    pub number_per: Option<String>,
144    /// Total cost number.
145    pub number_total: Option<String>,
146    /// Cost currency.
147    pub currency: Option<String>,
148    /// Acquisition date.
149    pub date: Option<String>,
150    /// Lot label.
151    pub label: Option<String>,
152    /// Merge lots flag.
153    pub merge: bool,
154}
155
156/// Price annotation data.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PriceAnnotationData {
159    /// Whether this is a total price (@@) vs per-unit (@).
160    pub is_total: bool,
161    /// The price amount (optional for incomplete/empty prices).
162    pub amount: Option<AmountData>,
163    /// The number only (for incomplete prices).
164    pub number: Option<String>,
165    /// The currency only (for incomplete prices).
166    pub currency: Option<String>,
167}
168
169/// Metadata value for serialization.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(tag = "type", content = "value")]
172pub enum MetaValueData {
173    /// String value.
174    #[serde(rename = "string")]
175    String(String),
176    /// Number value.
177    #[serde(rename = "number")]
178    Number(String),
179    /// Date value.
180    #[serde(rename = "date")]
181    Date(String),
182    /// Account reference.
183    #[serde(rename = "account")]
184    Account(String),
185    /// Currency reference.
186    #[serde(rename = "currency")]
187    Currency(String),
188    /// Tag reference.
189    #[serde(rename = "tag")]
190    Tag(String),
191    /// Link reference.
192    #[serde(rename = "link")]
193    Link(String),
194    /// Amount value.
195    #[serde(rename = "amount")]
196    Amount(AmountData),
197    /// Boolean value.
198    #[serde(rename = "bool")]
199    Bool(bool),
200}
201
202/// Balance assertion data.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct BalanceData {
205    /// Account name.
206    pub account: String,
207    /// Expected balance.
208    pub amount: AmountData,
209    /// Tolerance.
210    pub tolerance: Option<String>,
211    /// Metadata key-value pairs.
212    #[serde(default)]
213    pub metadata: Vec<(String, MetaValueData)>,
214}
215
216/// Open account data.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct OpenData {
219    /// Account name.
220    pub account: String,
221    /// Allowed currencies.
222    pub currencies: Vec<String>,
223    /// Booking method.
224    pub booking: Option<String>,
225    /// Metadata key-value pairs.
226    #[serde(default)]
227    pub metadata: Vec<(String, MetaValueData)>,
228}
229
230/// Close account data.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct CloseData {
233    /// Account name.
234    pub account: String,
235    /// Metadata key-value pairs.
236    #[serde(default)]
237    pub metadata: Vec<(String, MetaValueData)>,
238}
239
240/// Commodity declaration data.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct CommodityData {
243    /// Currency code.
244    pub currency: String,
245    /// Metadata key-value pairs.
246    #[serde(default)]
247    pub metadata: Vec<(String, MetaValueData)>,
248}
249
250/// Pad directive data.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct PadData {
253    /// Account to pad.
254    pub account: String,
255    /// Source account for padding.
256    pub source_account: String,
257    /// Metadata key-value pairs.
258    #[serde(default)]
259    pub metadata: Vec<(String, MetaValueData)>,
260}
261
262/// Event data.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct EventData {
265    /// Event type.
266    pub event_type: String,
267    /// Event value.
268    pub value: String,
269    /// Metadata key-value pairs.
270    #[serde(default)]
271    pub metadata: Vec<(String, MetaValueData)>,
272}
273
274/// Note data.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct NoteData {
277    /// Account name.
278    pub account: String,
279    /// Note comment.
280    pub comment: String,
281    /// Metadata key-value pairs.
282    #[serde(default)]
283    pub metadata: Vec<(String, MetaValueData)>,
284}
285
286/// Document data.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct DocumentData {
289    /// Account name.
290    pub account: String,
291    /// Document path.
292    pub path: String,
293    /// Metadata key-value pairs.
294    #[serde(default)]
295    pub metadata: Vec<(String, MetaValueData)>,
296}
297
298/// Price directive data.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct PriceData {
301    /// Currency being priced.
302    pub currency: String,
303    /// Price amount.
304    pub amount: AmountData,
305    /// Metadata key-value pairs.
306    #[serde(default)]
307    pub metadata: Vec<(String, MetaValueData)>,
308}
309
310/// Query directive data.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct QueryData {
313    /// Query name.
314    pub name: String,
315    /// Query string.
316    pub query: String,
317    /// Metadata key-value pairs.
318    #[serde(default)]
319    pub metadata: Vec<(String, MetaValueData)>,
320}
321
322/// Custom directive data.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct CustomData {
325    /// Custom type.
326    pub custom_type: String,
327    /// Values preserving their types (Account, Amount, String, etc.).
328    pub values: Vec<MetaValueData>,
329    /// Metadata key-value pairs.
330    #[serde(default)]
331    pub metadata: Vec<(String, MetaValueData)>,
332}
333
334/// Ledger options passed to plugins.
335#[derive(Debug, Clone, Default, Serialize, Deserialize)]
336pub struct PluginOptions {
337    /// Operating currencies.
338    pub operating_currencies: Vec<String>,
339    /// Ledger title.
340    pub title: Option<String>,
341}
342
343/// Error generated by a plugin.
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct PluginError {
346    /// Error message.
347    pub message: String,
348    /// Source file (if known).
349    pub source_file: Option<String>,
350    /// Line number (if known).
351    pub line_number: Option<u32>,
352    /// Error severity.
353    pub severity: PluginErrorSeverity,
354}
355
356/// Severity of a plugin error.
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
358pub enum PluginErrorSeverity {
359    /// Warning - processing continues.
360    #[serde(rename = "warning")]
361    Warning,
362    /// Error - ledger is marked invalid.
363    #[serde(rename = "error")]
364    Error,
365}
366
367impl PluginError {
368    /// Create a new error.
369    pub fn error(message: impl Into<String>) -> Self {
370        Self {
371            message: message.into(),
372            source_file: None,
373            line_number: None,
374            severity: PluginErrorSeverity::Error,
375        }
376    }
377
378    /// Create a new warning.
379    pub fn warning(message: impl Into<String>) -> Self {
380        Self {
381            message: message.into(),
382            source_file: None,
383            line_number: None,
384            severity: PluginErrorSeverity::Warning,
385        }
386    }
387
388    /// Set the source location.
389    pub fn at(mut self, file: impl Into<String>, line: u32) -> Self {
390        self.source_file = Some(file.into());
391        self.line_number = Some(line);
392        self
393    }
394}
395
396impl PluginOutput {
397    /// Create an empty output with the original directives.
398    pub const fn passthrough(directives: Vec<DirectiveWrapper>) -> Self {
399        Self {
400            directives,
401            errors: Vec::new(),
402        }
403    }
404}
405
406impl DirectiveWrapper {
407    /// Returns the sort order for directive types, matching Python beancount's `SORT_ORDER`.
408    ///
409    /// Order ensures logical processing:
410    /// - Open (-2): Accounts must be opened first
411    /// - Balance (-1): Balance assertions checked before transactions
412    /// - Default (0): Transactions, Commodity, Pad, Event, Note, Price, Query, Custom
413    /// - Document (1): Documents recorded after transactions
414    /// - Close (2): Accounts closed last
415    pub const fn type_sort_order(&self) -> i8 {
416        match &self.data {
417            DirectiveData::Open(_) => -2,
418            DirectiveData::Balance(_) => -1,
419            DirectiveData::Document(_) => 1,
420            DirectiveData::Close(_) => 2,
421            _ => 0, // Transaction, Commodity, Pad, Event, Note, Price, Query, Custom
422        }
423    }
424
425    /// Returns a sort key tuple matching Python beancount's `entry_sortkey()`.
426    ///
427    /// Sorts by: (date, `type_order`, lineno)
428    /// This ensures deterministic ordering for entries on the same date.
429    pub fn sort_key(&self) -> (&str, i8, u32) {
430        (
431            &self.date,
432            self.type_sort_order(),
433            self.lineno.unwrap_or(u32::MAX), // Plugin-generated entries sort last
434        )
435    }
436}
437
438/// Sort directives using beancount's standard ordering.
439///
440/// This matches Python beancount's `entry_sortkey()`:
441/// 1. Primary: date
442/// 2. Secondary: directive type (Open, Balance, default, Document, Close)
443/// 3. Tertiary: line number (preserves file order for same-date, same-type entries)
444pub fn sort_directives(directives: &mut [DirectiveWrapper]) {
445    directives.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
446}