rustledger-ffi-wasi 0.21.0

Slimmed FFI-support helpers reused by the rustledger WASI component FFI (loader orchestration + WIT-input construction + directive hashing; the wasip1 JSON-RPC surface and Directive-to-JSON DTO were removed in #1419)
Documentation
//! Output types for JSON serialization.

use std::collections::HashMap;

use serde::Serialize;

#[derive(Serialize, Clone)]
pub struct Error {
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub column: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub entry_index: Option<usize>,
    pub severity: String,
    /// Processing phase that produced this error.
    ///
    /// Known values:
    /// - `"parse"` — syntax/load errors (default when constructed via `Error::new()`)
    /// - `"validate"` — semantic validation errors (set via `.validate_phase()`)
    ///
    /// Non-ledger contexts (e.g., query parsing) reuse the default `"parse"` value,
    /// which is acceptable since those errors are also syntactic in nature.
    pub phase: String,
}

impl Error {
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            line: None,
            column: None,
            field: None,
            entry_index: None,
            severity: "error".to_string(),
            phase: "parse".to_string(),
        }
    }

    pub const fn with_line(mut self, line: u32) -> Self {
        self.line = Some(line);
        self
    }

    pub fn validate_phase(mut self) -> Self {
        self.phase = "validate".to_string();
        self
    }
}

#[derive(Serialize)]
pub struct LedgerOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    pub operating_currency: Vec<String>,
    pub name_assets: String,
    pub name_liabilities: String,
    pub name_equity: String,
    pub name_income: String,
    pub name_expenses: String,
    pub documents: Vec<String>,
    pub commodities: Vec<String>,
    pub booking_method: String,
    pub display_precision: HashMap<String, u32>,
    /// Whether to render commas in numbers.
    pub render_commas: bool,
    /// Default tolerances per currency (e.g., {"USD": "0.005", "*": "0.01"}).
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub inferred_tolerance_default: HashMap<String, String>,
    /// Tolerance multiplier (default 0.5).
    pub inferred_tolerance_multiplier: String,
    /// Whether to infer tolerance from cost.
    pub infer_tolerance_from_cost: bool,
    /// Account for rounding errors.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub account_rounding: Option<String>,
    /// Account for previous balances (opening balances).
    pub account_previous_balances: String,
    /// Account for previous earnings.
    pub account_previous_earnings: String,
    /// Account for previous conversions.
    pub account_previous_conversions: String,
    /// Account for current earnings.
    pub account_current_earnings: String,
    /// Account for current conversion differences.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub account_current_conversions: Option<String>,
    /// Account for unrealized gains.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub account_unrealized_gains: Option<String>,
    /// Currency for conversion (if specified).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversion_currency: Option<String>,
}

impl Default for LedgerOptions {
    fn default() -> Self {
        Self {
            title: None,
            operating_currency: Vec::new(),
            name_assets: "Assets".to_string(),
            name_liabilities: "Liabilities".to_string(),
            name_equity: "Equity".to_string(),
            name_income: "Income".to_string(),
            name_expenses: "Expenses".to_string(),
            documents: Vec::new(),
            commodities: Vec::new(),
            booking_method: "STRICT".to_string(),
            display_precision: HashMap::new(),
            render_commas: false,
            inferred_tolerance_default: HashMap::new(),
            inferred_tolerance_multiplier: "0.5".to_string(),
            infer_tolerance_from_cost: false,
            account_rounding: None,
            account_previous_balances: "Equity:Opening-Balances".to_string(),
            account_previous_earnings: "Equity:Earnings:Previous".to_string(),
            account_previous_conversions: "Equity:Conversions:Previous".to_string(),
            account_current_earnings: "Equity:Earnings:Current".to_string(),
            account_current_conversions: None,
            account_unrealized_gains: None,
            conversion_currency: None,
        }
    }
}

/// A plugin directive from the source file.
#[derive(Serialize)]
pub struct Plugin {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub config: Option<String>,
}

/// An include directive from the source file.
#[derive(Serialize)]
pub struct Include {
    pub path: String,
    pub lineno: u32,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_default_phase_is_parse() {
        let err = Error::new("test");
        assert_eq!(err.phase, "parse");
    }

    #[test]
    fn test_error_validate_phase() {
        let err = Error::new("test").validate_phase();
        assert_eq!(err.phase, "validate");
    }

    #[test]
    fn test_error_phase_serializes() {
        let err = Error::new("test");
        let json = serde_json::to_value(&err).unwrap();
        assert_eq!(json["phase"], "parse");

        let err = Error::new("test").validate_phase();
        let json = serde_json::to_value(&err).unwrap();
        assert_eq!(json["phase"], "validate");
    }
}