use axum::response::AppendHeaders;
use axum::{extract::Query, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use base64::{engine::general_purpose, Engine};
use serde::Serialize;
use std::collections::HashMap;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing::Level;
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt};
extern crate base64;
use dotenvy::dotenv;
use tokio::process::Command;
#[tokio::main]
async fn main() {
dotenv().ok();
initialize_logging();
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let app = Router::new()
.route("/", get(route_rust_ledger))
.route("/hello", get(hello_img))
.route("/ping", get(|| async { "pong" }))
.route("/infrastructure/config", get(get_config))
.route("/infrastructure/accounts", get(get_accounts))
.route("/infrastructure/commodities", get(get_commodities))
.route("/shutdown", get(shutdown))
.layer(cors)
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
log::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
fn initialize_logging() {
let formatting_layer = tracing_subscriber::fmt::layer();
let filter = filter::Targets::new()
.with_target("cashier_server", Level::TRACE)
.with_target("tower_http::trace::on_response", Level::DEBUG)
.with_target("tower_http::trace::make_span", Level::DEBUG)
.with_default(Level::INFO);
tracing_subscriber::registry()
.with(formatting_layer)
.with(filter)
.init();
}
async fn hello_img() -> impl IntoResponse {
let pixel_encoded = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let decoded = general_purpose::STANDARD.decode(pixel_encoded);
(
AppendHeaders([(axum::http::header::CONTENT_TYPE, "image/png")]),
decoded.unwrap(),
)
}
async fn route_rust_ledger(Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
log::debug!("rust_ledger: {:?}", params);
if !params.contains_key("query") {
return (
StatusCode::BAD_REQUEST,
[(
axum::http::header::CONTENT_TYPE,
"application/json",
)],
serde_json::json!({"error": "No query provided"}).to_string(),
)
.into_response();
}
let query = params["query"].as_str();
let ledger_output = run_rust_ledger(query).await;
(
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
"application/json",
)],
ledger_output,
)
.into_response()
}
#[derive(Serialize)]
struct InfrastructureResponse {
content: String,
}
async fn get_config() -> impl IntoResponse {
read_infrastructure_file("config.bean").await
}
async fn get_accounts() -> impl IntoResponse {
read_infrastructure_file("accounts.bean").await
}
async fn get_commodities() -> impl IntoResponse {
read_infrastructure_file("commodities.bean").await
}
async fn read_infrastructure_file(filename: &str) -> impl IntoResponse {
let bean_file = match std::env::var("LEDGER_FILE") {
Ok(v) => v,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"LEDGER_FILE environment variable not set",
)
.into_response()
}
};
let path = std::path::Path::new(&bean_file);
let parent = match path.parent() {
Some(p) => p,
None => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid LEDGER_FILE path",
)
.into_response()
}
};
let file_path = parent.join(filename);
match tokio::fs::read_to_string(file_path).await {
Ok(content) => (StatusCode::OK, Json(InfrastructureResponse { content })).into_response(),
Err(e) => (StatusCode::NOT_FOUND, format!("File not found: {}", e)).into_response(),
}
}
async fn run_rust_ledger(query: &str) -> String {
let bean_file = match std::env::var("LEDGER_FILE") {
Ok(v) => v,
Err(_) => return String::from("LEDGER_FILE environment variable not set"),
};
let mut rledger = Command::new("rledger");
rledger.args(["query", "-f", "json", &bean_file, query]);
let output = rledger.output().await.expect("failed to execute process");
let result: String;
if !output.status.success() {
result = String::from_utf8_lossy(&output.stderr).to_string();
} else {
result = String::from_utf8_lossy(&output.stdout).to_string();
}
return result;
}
async fn shutdown() {
let msg = "Shutting down on client request...";
tracing::warn!(msg);
std::process::exit(0);
}