Add JSON export for full data dump
This commit is contained in:
@@ -469,6 +469,21 @@ impl Database {
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn list_all_budgets(&self) -> SqlResult<Vec<Budget>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, category_id, amount, month FROM budgets ORDER BY month ASC, id ASC"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(Budget {
|
||||
id: row.get(0)?,
|
||||
category_id: row.get(1)?,
|
||||
amount: row.get(2)?,
|
||||
month: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn delete_budget(&self, id: i64) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM budgets WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::db::Database;
|
||||
use crate::models::{Budget, Category, RecurringTransaction, Transaction};
|
||||
use serde::Serialize;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ExportData {
|
||||
pub transactions: Vec<Transaction>,
|
||||
pub categories: Vec<Category>,
|
||||
pub budgets: Vec<Budget>,
|
||||
pub recurring: Vec<RecurringTransaction>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExportError {
|
||||
Db(rusqlite::Error),
|
||||
Json(serde_json::Error),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for ExportError {
|
||||
fn from(e: rusqlite::Error) -> Self {
|
||||
ExportError::Db(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ExportError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
ExportError::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ExportError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
ExportError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExportError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExportError::Db(e) => write!(f, "Database error: {}", e),
|
||||
ExportError::Json(e) => write!(f, "JSON error: {}", e),
|
||||
ExportError::Io(e) => write!(f, "IO error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_json<W: Write>(db: &Database, writer: W) -> Result<ExportData, ExportError> {
|
||||
let transactions = db.list_all_transactions(None, None)?;
|
||||
let categories = db.list_categories(None)?;
|
||||
let budgets = db.list_all_budgets()?;
|
||||
let recurring = db.list_recurring(false)?;
|
||||
|
||||
let data = ExportData {
|
||||
transactions,
|
||||
categories,
|
||||
budgets,
|
||||
recurring,
|
||||
};
|
||||
|
||||
serde_json::to_writer_pretty(writer, &data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::{NewTransaction, TransactionType};
|
||||
use chrono::NaiveDate;
|
||||
|
||||
fn setup_db() -> Database {
|
||||
Database::open_in_memory().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_export_produces_valid_json() {
|
||||
let db = setup_db();
|
||||
let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
|
||||
|
||||
let txn = NewTransaction {
|
||||
amount: 25.0,
|
||||
transaction_type: TransactionType::Expense,
|
||||
category_id: cats[0].id,
|
||||
currency: "USD".to_string(),
|
||||
exchange_rate: 1.0,
|
||||
note: Some("Test".to_string()),
|
||||
date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
|
||||
recurring_id: None,
|
||||
};
|
||||
db.insert_transaction(&txn).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let data = export_json(&db, &mut buf).unwrap();
|
||||
|
||||
assert_eq!(data.transactions.len(), 1);
|
||||
assert!(!data.categories.is_empty());
|
||||
assert!(data.budgets.is_empty());
|
||||
assert!(data.recurring.is_empty());
|
||||
|
||||
// Verify it's valid JSON by parsing it back
|
||||
let output = String::from_utf8(buf).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
|
||||
assert!(parsed["transactions"].is_array());
|
||||
assert!(parsed["categories"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_export_includes_all_sections() {
|
||||
let db = setup_db();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let data = export_json(&db, &mut buf).unwrap();
|
||||
|
||||
// Even with no transactions, we should have default categories
|
||||
assert!(!data.categories.is_empty());
|
||||
|
||||
let output = String::from_utf8(buf).unwrap();
|
||||
assert!(output.contains("\"transactions\""));
|
||||
assert!(output.contains("\"categories\""));
|
||||
assert!(output.contains("\"budgets\""));
|
||||
assert!(output.contains("\"recurring\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_export_transaction_fields() {
|
||||
let db = setup_db();
|
||||
let cats = db.list_categories(Some(TransactionType::Income)).unwrap();
|
||||
|
||||
let txn = NewTransaction {
|
||||
amount: 500.0,
|
||||
transaction_type: TransactionType::Income,
|
||||
category_id: cats[0].id,
|
||||
currency: "EUR".to_string(),
|
||||
exchange_rate: 0.92,
|
||||
note: Some("Freelance".to_string()),
|
||||
date: NaiveDate::from_ymd_opt(2026, 2, 15).unwrap(),
|
||||
recurring_id: None,
|
||||
};
|
||||
db.insert_transaction(&txn).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
export_json(&db, &mut buf).unwrap();
|
||||
|
||||
let output = String::from_utf8(buf).unwrap();
|
||||
assert!(output.contains("500.0"));
|
||||
assert!(output.contains("EUR"));
|
||||
assert!(output.contains("Freelance"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user