From 1d5bdfe920fbf01fa6a107197a72edd939f285da Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 11 Mar 2025 07:49:34 -0500 Subject: [PATCH] transactions: Add JSON response format Rocket makes it easy to dispatch requests for the same path to different methods based on the `Accept` request header via the `format` argument to the request attribute. We'll use this JSON response format to populate a dropdown on the Add Receipt form, which can be used to automatically fill the fields for receipt data based on an existing transaction. --- src/main.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 64960ea..b5b9379 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,13 @@ mod config; mod firefly; mod receipts; +use chrono::{DateTime, FixedOffset}; use rocket::fairing::{self, AdHoc}; use rocket::form::Form; use rocket::fs::{FileServer, TempFile}; use rocket::http::Status; -use rocket::response::Redirect; +use rocket::response::{Redirect, Responder}; +use rocket::serde::json::Json; use rocket::tokio::io::{AsyncReadExt, BufReader}; use rocket::{Rocket, State}; use rocket_db_pools::Database as RocketDatabase; @@ -49,7 +51,7 @@ pub struct Transaction { pub id: String, pub amount: f64, pub description: String, - pub date: String, + pub date: DateTime, } #[derive(rocket::FromForm)] @@ -59,6 +61,27 @@ struct TransactionPostData<'r> { photo: Vec>, } +#[derive(Serialize)] +struct ErrorBody { + error: String, +} + +#[derive(Responder)] +#[response(status = 500)] +struct ErrorResponse { + error: Json, +} + +impl ErrorResponse { + pub fn new>(error: S) -> Self { + Self { + error: Json(ErrorBody { + error: error.into(), + }), + } + } +} + impl TryFrom for Transaction { type Error = &'static str; @@ -81,7 +104,7 @@ impl TryFrom for Transaction { id: t.id, amount, description, - date: date.format("%A, %_d %B %Y").to_string(), + date, }) } } @@ -91,7 +114,7 @@ async fn index() -> Redirect { Redirect::to("/receipts") } -#[rocket::get("/transactions")] +#[rocket::get("/transactions", format = "html", rank = 2)] async fn transaction_list(ctx: &State) -> (Status, Template) { let result = ctx .firefly @@ -136,6 +159,36 @@ async fn transaction_list(ctx: &State) -> (Status, Template) { } } +#[rocket::get("/transactions", format = "json")] +async fn transaction_list_json( + ctx: &State, +) -> Result>, ErrorResponse> { + let result = ctx + .firefly + .search_transactions(&ctx.config.firefly.search_query) + .await + .map_err(|e| { + error!("Error fetching transaction list: {}", e); + ErrorResponse::new( + "Failed to fetch transaction list from Firefly III", + ) + })?; + + Ok(Json( + result + .data + .into_iter() + .filter_map(|t| match Transaction::try_from(t) { + Ok(t) => Some(t), + Err(e) => { + error!("Error parsing transaction details: {}", e); + None + }, + }) + .collect(), + )) +} + #[rocket::get("/transactions/")] async fn get_transaction(id: &str, ctx: &State) -> Option