Skip to main content

rustledger_wasm/
types.rs

1//! Data transfer objects for WASM serialization.
2//!
3//! These types provide a JavaScript-friendly representation of Beancount data,
4//! using string representations for dates and numbers.
5
6use serde::{Deserialize, Serialize};
7
8/// Result of parsing a Beancount file.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ParseResult {
11    /// The parsed ledger (if successful).
12    pub ledger: Option<Ledger>,
13    /// Parse errors.
14    pub errors: Vec<Error>,
15}
16
17/// A parsed Beancount ledger.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Ledger {
20    /// All directives in the ledger.
21    pub directives: Vec<DirectiveJson>,
22    /// Ledger options.
23    pub options: LedgerOptions,
24}
25
26/// Ledger options.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct LedgerOptions {
29    /// Operating currencies.
30    pub operating_currencies: Vec<String>,
31    /// Ledger title.
32    pub title: Option<String>,
33}
34
35/// A directive in JSON-serializable form.
36///
37/// Each variant corresponds to a Beancount directive type, with fields
38/// representing the directive's data in a JavaScript-friendly format.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(tag = "type")]
41#[allow(missing_docs)]
42pub enum DirectiveJson {
43    /// Transaction directive.
44    #[serde(rename = "transaction")]
45    Transaction {
46        date: String,
47        flag: String,
48        payee: Option<String>,
49        narration: Option<String>,
50        tags: Vec<String>,
51        links: Vec<String>,
52        postings: Vec<PostingJson>,
53    },
54    /// Balance assertion.
55    #[serde(rename = "balance")]
56    Balance {
57        date: String,
58        account: String,
59        amount: AmountValue,
60    },
61    /// Open account.
62    #[serde(rename = "open")]
63    Open {
64        date: String,
65        account: String,
66        currencies: Vec<String>,
67        #[serde(skip_serializing_if = "Option::is_none")]
68        booking: Option<String>,
69    },
70    /// Close account.
71    #[serde(rename = "close")]
72    Close { date: String, account: String },
73    /// Commodity declaration.
74    #[serde(rename = "commodity")]
75    Commodity { date: String, currency: String },
76    /// Pad directive.
77    #[serde(rename = "pad")]
78    Pad {
79        date: String,
80        account: String,
81        source_account: String,
82    },
83    /// Event directive.
84    #[serde(rename = "event")]
85    Event {
86        date: String,
87        event_type: String,
88        value: String,
89    },
90    /// Note directive.
91    #[serde(rename = "note")]
92    Note {
93        date: String,
94        account: String,
95        comment: String,
96    },
97    /// Document directive.
98    #[serde(rename = "document")]
99    Document {
100        date: String,
101        account: String,
102        path: String,
103    },
104    /// Price directive.
105    #[serde(rename = "price")]
106    Price {
107        date: String,
108        currency: String,
109        amount: AmountValue,
110    },
111    /// Query directive.
112    #[serde(rename = "query")]
113    Query {
114        date: String,
115        name: String,
116        query_string: String,
117    },
118    /// Custom directive.
119    #[serde(rename = "custom")]
120    Custom { date: String, custom_type: String },
121}
122
123/// A posting in JSON-serializable form.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct PostingJson {
126    /// Account name.
127    pub account: String,
128    /// Units (amount).
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub units: Option<AmountValue>,
131    /// Cost specification.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub cost: Option<PostingCostJson>,
134    /// Price annotation.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub price: Option<AmountValue>,
137}
138
139/// A posting cost in JSON-serializable form.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct PostingCostJson {
142    /// Cost per unit.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub number_per: Option<String>,
145    /// Cost currency.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub currency: Option<String>,
148    /// Acquisition date.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub date: Option<String>,
151    /// Lot label.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub label: Option<String>,
154}
155
156/// Error severity level.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "lowercase")]
159pub enum Severity {
160    /// An error that prevents processing.
161    Error,
162    /// A warning that doesn't prevent processing.
163    Warning,
164}
165
166/// An error with source location.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct Error {
169    /// Error message.
170    pub message: String,
171    /// Line number (1-based).
172    pub line: Option<u32>,
173    /// Column number (1-based).
174    pub column: Option<u32>,
175    /// Error severity.
176    pub severity: Severity,
177}
178
179impl Error {
180    /// Create a new error with a message.
181    pub fn new(message: impl Into<String>) -> Self {
182        Self {
183            message: message.into(),
184            line: None,
185            column: None,
186            severity: Severity::Error,
187        }
188    }
189
190    /// Create an error with a line number.
191    pub fn with_line(message: impl Into<String>, line: u32) -> Self {
192        Self {
193            message: message.into(),
194            line: Some(line),
195            column: None,
196            severity: Severity::Error,
197        }
198    }
199
200    /// Create a warning.
201    pub fn warning(message: impl Into<String>) -> Self {
202        Self {
203            message: message.into(),
204            line: None,
205            column: None,
206            severity: Severity::Warning,
207        }
208    }
209}
210
211/// Result of validation.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ValidationResult {
214    /// Whether the ledger is valid.
215    pub valid: bool,
216    /// Validation errors.
217    pub errors: Vec<Error>,
218}
219
220/// Result of a BQL query.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct QueryResult {
223    /// Column names.
224    pub columns: Vec<String>,
225    /// Result rows.
226    pub rows: Vec<Vec<CellValue>>,
227    /// Query errors.
228    pub errors: Vec<Error>,
229}
230
231/// A cell value that serializes properly to JavaScript.
232///
233/// Uses untagged serialization to produce clean JSON output.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(untagged)]
236#[allow(missing_docs)]
237pub enum CellValue {
238    /// Null value.
239    Null,
240    /// String value.
241    String(String),
242    /// Integer value.
243    Integer(i64),
244    /// Boolean value.
245    Boolean(bool),
246    /// Amount with number and currency.
247    Amount { number: String, currency: String },
248    /// Position with units and optional cost.
249    Position {
250        units: AmountValue,
251        #[serde(skip_serializing_if = "Option::is_none")]
252        cost: Option<CostValue>,
253    },
254    /// Inventory with positions.
255    Inventory { positions: Vec<PositionValue> },
256    /// Set of strings.
257    StringSet(Vec<String>),
258    /// Object with key-value pairs (for `entry` and `meta` columns).
259    Object(std::collections::HashMap<String, Box<Self>>),
260}
261
262/// Amount value for serialization.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct AmountValue {
265    /// The number as a string.
266    pub number: String,
267    /// The currency.
268    pub currency: String,
269}
270
271/// Position value for serialization.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct PositionValue {
274    /// The units.
275    pub units: AmountValue,
276}
277
278/// Cost value for serialization.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct CostValue {
281    /// Cost per unit.
282    pub number: String,
283    /// Cost currency.
284    pub currency: String,
285    /// Acquisition date.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub date: Option<String>,
288    /// Lot label.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub label: Option<String>,
291}
292
293/// Result of formatting.
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct FormatResult {
296    /// Formatted source (if successful).
297    pub formatted: Option<String>,
298    /// Format errors.
299    pub errors: Vec<Error>,
300}
301
302/// Result of pad expansion.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct PadResult {
305    /// Directives with pads removed.
306    pub directives: Vec<DirectiveJson>,
307    /// Generated padding transactions.
308    pub padding_transactions: Vec<DirectiveJson>,
309    /// Pad processing errors.
310    pub errors: Vec<Error>,
311}
312
313/// Result of running a plugin.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct PluginResult {
316    /// Modified directives.
317    pub directives: Vec<DirectiveJson>,
318    /// Plugin errors/warnings.
319    pub errors: Vec<Error>,
320}
321
322/// Plugin information.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct PluginInfo {
325    /// Plugin name.
326    pub name: String,
327    /// Plugin description.
328    pub description: String,
329}
330
331/// BQL completion suggestion for WASM.
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct CompletionJson {
334    /// The completion text to insert.
335    pub text: String,
336    /// Category: keyword, function, column, operator, literal.
337    pub category: String,
338    /// Optional description/documentation.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub description: Option<String>,
341}
342
343/// Result of BQL completion request.
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct CompletionResultJson {
346    /// List of completions.
347    pub completions: Vec<CompletionJson>,
348    /// Current context for debugging.
349    pub context: String,
350}
351
352// =============================================================================
353// LSP-like Types for Editor Integration
354// =============================================================================
355
356/// A completion item for Beancount source editing.
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct EditorCompletion {
359    /// The label to display in the completion list.
360    pub label: String,
361    /// The kind of completion item.
362    pub kind: CompletionKind,
363    /// A human-readable string with additional information.
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub detail: Option<String>,
366    /// The text to insert when this completion is selected.
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub insert_text: Option<String>,
369}
370
371/// The kind of a completion item.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(rename_all = "lowercase")]
374pub enum CompletionKind {
375    /// A keyword (directive name).
376    Keyword,
377    /// An account name.
378    Account,
379    /// An account segment (partial account).
380    AccountSegment,
381    /// A currency/commodity.
382    Currency,
383    /// A payee name.
384    Payee,
385    /// A date value.
386    Date,
387    /// A text/string value.
388    Text,
389}
390
391/// Result of a completion request.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct EditorCompletionResult {
394    /// The completions.
395    pub completions: Vec<EditorCompletion>,
396    /// The detected context.
397    pub context: String,
398}
399
400/// Hover information for a symbol.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct EditorHoverInfo {
403    /// The hover content (Markdown formatted).
404    pub contents: String,
405    /// The range of the hovered symbol (optional).
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub range: Option<EditorRange>,
408}
409
410/// A range in the document.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct EditorRange {
413    /// Start line (0-based).
414    pub start_line: u32,
415    /// Start character (0-based).
416    pub start_character: u32,
417    /// End line (0-based).
418    pub end_line: u32,
419    /// End character (0-based).
420    pub end_character: u32,
421}
422
423/// A location in the document.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct EditorLocation {
426    /// Line number (0-based).
427    pub line: u32,
428    /// Character offset (0-based).
429    pub character: u32,
430}
431
432/// A document symbol for the outline view.
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct EditorDocumentSymbol {
435    /// The name of this symbol.
436    pub name: String,
437    /// More detail for this symbol.
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub detail: Option<String>,
440    /// The kind of this symbol.
441    pub kind: SymbolKind,
442    /// The range enclosing this symbol.
443    pub range: EditorRange,
444    /// Children of this symbol (e.g., postings in a transaction).
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub children: Option<Vec<Self>>,
447    /// Whether this symbol is deprecated (e.g., closed account).
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub deprecated: Option<bool>,
450}
451
452/// The kind of a symbol.
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
454#[serde(rename_all = "lowercase")]
455pub enum SymbolKind {
456    /// A transaction.
457    Transaction,
458    /// An account (open/close).
459    Account,
460    /// A balance assertion.
461    Balance,
462    /// A commodity/currency declaration.
463    Commodity,
464    /// A posting within a transaction.
465    Posting,
466    /// A pad directive.
467    Pad,
468    /// An event.
469    Event,
470    /// A note.
471    Note,
472    /// A document link.
473    Document,
474    /// A price.
475    Price,
476    /// A query definition.
477    Query,
478    /// A custom directive.
479    Custom,
480}
481
482// =============================================================================
483// References Types
484// =============================================================================
485
486/// The kind of symbol being referenced.
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
488#[serde(rename_all = "lowercase")]
489pub enum ReferenceKind {
490    /// An account reference.
491    Account,
492    /// A currency/commodity reference.
493    Currency,
494    /// A payee reference.
495    Payee,
496}
497
498/// A reference to a symbol in the document.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct EditorReference {
501    /// The range of this reference.
502    pub range: EditorRange,
503    /// The kind of reference.
504    pub kind: ReferenceKind,
505    /// Whether this is the defining occurrence.
506    pub is_definition: bool,
507    /// Human-readable context (e.g., directive type).
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub context: Option<String>,
510}
511
512/// Result of a find-references request.
513#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct EditorReferencesResult {
515    /// The symbol being searched for.
516    pub symbol: String,
517    /// The kind of symbol.
518    pub kind: ReferenceKind,
519    /// All references found.
520    pub references: Vec<EditorReference>,
521}