refactored project for readability

This commit is contained in:
Steven 2023-08-04 20:09:09 -04:00
parent dc3b994247
commit aa4559be2a
7 changed files with 149 additions and 95 deletions

View file

@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use reqwest;
use prometheus::{GaugeVec, Opts};
use std::{env, fs::File, fs::OpenOptions, io::BufReader, collections::HashMap};
use crate::util;
lazy_static::lazy_static! {
pub static ref CARD_VALUES: GaugeVec = GaugeVec::new(
@ -51,7 +52,16 @@ pub async fn process_card(card_from_file: &mut CardFromFile) -> Result<(), Box<d
Ok(())
}
pub fn process_top_cards(cards: &mut Vec<CardFromFile>) {
pub fn get_data_update_interval() -> Result<u64, Box<dyn std::error::Error>> {
let update_interval = dotenv::var("UPDATE_INTERVAL")
.map_err(|_| "UPDATE_INTERVAL is not defined in the .env file")?
.parse::<u64>()
.map_err(|_| "UPDATE_INTERVAL is not a valid number")?;
Ok(update_interval * 3600)
}
pub fn process_export_data(cards: &mut Vec<CardFromFile>) {
cards.sort_by(|a, b| b.usd_value.partial_cmp(&a.usd_value).unwrap());
let top_10_cards = &cards[..10];
for card in top_10_cards {
@ -60,4 +70,35 @@ pub fn process_top_cards(cards: &mut Vec<CardFromFile>) {
CARD_VALUES.with_label_values(&[&card.name]).set(value);
}
}
}
pub async fn process_cards(interval: &mut tokio::time::Interval) -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Please provide the path to the JSON file as an argument.");
return Ok(());
}
let file_path = &args[1];
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut cards_data: CardFile = serde_json::from_reader(reader)?;
let pb = util::setup_progress_bar(cards_data.cards.len() as u64);
for card_from_file in &mut cards_data.cards {
process_card(card_from_file).await?;
pb.inc(1);
interval.tick().await;
}
process_export_data(&mut cards_data.cards);
pb.finish_with_message("Completed!");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)?;
serde_json::to_writer_pretty(file, &cards_data)?;
Ok(())
}

13
src/handlers.rs Normal file
View file

@ -0,0 +1,13 @@
use axum::response::IntoResponse;
use crate::templates::RootTemplate;
use crate::util::HtmlTemplate;
pub async fn root() -> impl IntoResponse {
let template = RootTemplate {};
HtmlTemplate(template)
}
pub async fn health() -> impl IntoResponse {
"200 OK"
}

View file

@ -1,71 +1,41 @@
use dotenv;
use std::{env, fs::File, fs::OpenOptions, io::BufReader, sync::Arc,net::SocketAddr};
use tokio::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
use prometheus::Registry;
use tracing::info;
use std::sync::Arc;
mod server;
mod card;
mod cards;
mod templates;
mod handlers;
mod util;
use server::run_metrics_server;
use card::{CARD_VALUES, CardFile, process_card, process_top_cards};
use server::run_server;
use cards::{CARD_VALUES,get_data_update_interval};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv::dotenv().ok();
let update_interval = dotenv::var("UPDATE_INTERVAL")
.map_err(|_| "UPDATE_INTERVAL is not defined in the .env file")?
.parse::<u64>()
.map_err(|_| "UPDATE_INTERVAL is not a valid number")?
* 3600;
let registry = Arc::new(Registry::new());
registry.register(Box::new(CARD_VALUES.clone())).unwrap();
let update_interval = get_data_update_interval()?;
let registry = setup_registry()?;
util::setup_tracing_subscriber();
let metrics_addr = SocketAddr::from(([127, 0, 0, 1], 3001));
tokio::spawn(run_metrics_server(metrics_addr, registry.clone()));
let ip_addr = server::get_ip_address()?;
tokio::spawn(run_server(ip_addr, registry.clone()));
info!("setting update interval {}", (update_interval/3600).to_string());
let mut interval = tokio::time::interval(Duration::from_secs(update_interval));
info!("server started, now listening on port {}", ip_addr.port());
loop {
interval.tick().await;
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Please provide the path to the JSON file as an argument.");
continue;
}
let file_path = &args[1];
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut cards_data: CardFile = serde_json::from_reader(reader)?;
let pb = setup_progress_bar(cards_data.cards.len() as u64);
for card_from_file in &mut cards_data.cards {
process_card(card_from_file).await?;
pb.inc(1);
tokio::time::sleep(Duration::from_millis(100)).await;
}
process_top_cards(&mut cards_data.cards);
pb.finish_with_message("Completed!");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)?;
serde_json::to_writer_pretty(file, &cards_data)?;
cards::process_cards(&mut interval).await?;
}
}
fn setup_progress_bar(len: u64) -> ProgressBar {
let pb = ProgressBar::new(len);
let style = ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-");
pb.set_style(style);
pb
}
fn setup_registry() -> Result<Arc<Registry>, Box<dyn std::error::Error>> {
let registry = Arc::new(Registry::new());
registry.register(Box::new(CARD_VALUES.clone())).unwrap();
Ok(registry)
}

View file

@ -1,63 +1,42 @@
use axum::{routing::get, Router,response::{Html, IntoResponse, Response},http::StatusCode,};
use axum::{routing::get, Router};
use hyper::{Server, http, body};
use tower_http::services::ServeDir;
use std::net::SocketAddr;
use prometheus::{Encoder, TextEncoder, Registry};
use std::sync::Arc;
use tower_http::services::ServeDir;
use askama::Template;
pub async fn run_metrics_server(addr: SocketAddr, registry: Arc<Registry>) {
use crate::handlers;
pub async fn run_server(addr: SocketAddr, registry: Arc<Registry>) {
let assets_path = std::env::current_dir().unwrap();
let app = Router::new()
.route("/", get(root))
.nest_service("/assets",ServeDir::new(format!("{}/assets", assets_path.to_str().unwrap())))
.route("/health", get(|| async { "OK" }))
.route("/", get(handlers::root))
.route("/health", get(handlers::health))
.route("/metrics", get(move || {
let registry = Arc::clone(&registry);
async move {
let metric_families = registry.gather();
let mut buffer = vec![];
let encoder = TextEncoder::new();
encoder.encode(&metric_families, &mut buffer).unwrap();
let metrics = String::from_utf8(buffer).unwrap();
http::Response::new(body::Body::from(metrics))
}
}));
Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
async fn root() -> impl IntoResponse {
let template = RootTemplate {};
HtmlTemplate(template)
}
pub fn get_ip_address() -> Result<SocketAddr, Box<dyn std::error::Error>> {
let ip_addr = SocketAddr::from(([127, 0, 0, 1],
dotenv::var("PORT")
.map_err(|_| "PORT is not defined in the .env file")?
.parse::<u16>()
.map_err(|_| "PORT is not a valid number")?
));
#[derive(Template)]
#[template(path = "root.html")]
struct RootTemplate;
/// A wrapper type that we'll use to encapsulate HTML parsed by askama into valid HTML for axum to serve.
struct HtmlTemplate<T>(T);
/// Allows us to convert Askama HTML templates into valid HTML for axum to serve in the response.
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
// Attempt to render the template with askama
match self.0.render() {
// If we're able to successfully parse and aggregate the template, serve it
Ok(html) => Html(html).into_response(),
// If we're not, return an error or some bit of fallback HTML
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {}", err),
)
.into_response(),
}
}
}
Ok(ip_addr)
}

5
src/templates.rs Normal file
View file

@ -0,0 +1,5 @@
use askama::Template;
#[derive(Template)]
#[template(path = "root.html")]
pub struct RootTemplate;

45
src/util.rs Normal file
View file

@ -0,0 +1,45 @@
use axum::{response::{Html,Response},http::StatusCode,response::IntoResponse};
use askama::Template;
use indicatif::{ProgressBar, ProgressStyle};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
/// A wrapper type that we'll use to encapsulate HTML parsed by askama into valid HTML for axum to serve.
pub struct HtmlTemplate<T>(pub T);
/// Allows us to convert Askama HTML templates into valid HTML for axum to serve in the response.
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
// Attempt to render the template with askama
match self.0.render() {
// If we're able to successfully parse and aggregate the template, serve it
Ok(html) => Html(html).into_response(),
// If we're not, return an error or some bit of fallback HTML
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {}", err),
)
.into_response(),
}
}
}
pub fn setup_progress_bar(len: u64) -> ProgressBar {
let pb = ProgressBar::new(len);
let style = ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#|.");
pb.set_style(style);
pb
}
pub fn setup_tracing_subscriber() {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "sk_tcg_trader".into()),)
.with(tracing_subscriber::fmt::layer())
.init();
}