From 25b3aec69efb9bdbc507b2721281fb23f76dc110 Mon Sep 17 00:00:00 2001 From: specCon18 Date: Wed, 2 Aug 2023 04:25:04 -0400 Subject: [PATCH] refactored project --- .direnv/flake-profile | 2 +- .direnv/flake-profile-4-link | 1 + cards.json | 2 +- src/card.rs | 63 ++++++++++++++ src/main.rs | 161 ++++++----------------------------- src/server.rs | 24 ++++++ 6 files changed, 116 insertions(+), 137 deletions(-) create mode 120000 .direnv/flake-profile-4-link create mode 100644 src/card.rs create mode 100644 src/server.rs diff --git a/.direnv/flake-profile b/.direnv/flake-profile index 519b17b..e289079 120000 --- a/.direnv/flake-profile +++ b/.direnv/flake-profile @@ -1 +1 @@ -flake-profile-3-link \ No newline at end of file +flake-profile-4-link \ No newline at end of file diff --git a/.direnv/flake-profile-4-link b/.direnv/flake-profile-4-link new file mode 120000 index 0000000..dcd9e5f --- /dev/null +++ b/.direnv/flake-profile-4-link @@ -0,0 +1 @@ +/nix/store/c7g4wladsws7hyv4bivpkp3pkad8300h-my-rust-project-env \ No newline at end of file diff --git a/cards.json b/cards.json index 7031cb5..b25c609 100644 --- a/cards.json +++ b/cards.json @@ -688,7 +688,7 @@ { "name": "phyrexian scriptures", "count": 1, - "usd_value": "0.80" + "usd_value": "0.81" }, { "name": "snow-covered island", diff --git a/src/card.rs b/src/card.rs new file mode 100644 index 0000000..723f06a --- /dev/null +++ b/src/card.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use reqwest; +use prometheus::{GaugeVec, Opts}; + +lazy_static::lazy_static! { + pub static ref CARD_VALUES: GaugeVec = GaugeVec::new( + Opts::new("card_value", "The value of cards"), + &["name"] + ).unwrap(); +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Card { + name: String, + prices: HashMap>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CardFromFile { + name: String, + count: usize, + usd_value: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CardFile { + pub cards: Vec, +} + +pub async fn process_card(card_from_file: &mut CardFromFile) -> Result<(), Box> { + let request_url = format!( + "https://api.scryfall.com/cards/named?exact={}", + card_from_file.name + ); + let response = reqwest::get(&request_url).await?; + let card: Card = response.json().await?; + + if let Some(price) = card.prices.get("usd") { + if let Some(price_str) = price { + card_from_file.usd_value = Some(price_str.clone()); + + let value = price_str.parse::().unwrap(); + CARD_VALUES.with_label_values(&[&card_from_file.name]).set(value); + } else { + card_from_file.usd_value = Some("0.0".to_string()); + CARD_VALUES.with_label_values(&[&card_from_file.name]).set(0.0); + } + } + + Ok(()) +} + +pub fn process_top_cards(cards: &mut Vec) { + 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 { + if let Some(price_str) = &card.usd_value { + let value = price_str.parse::().unwrap(); + CARD_VALUES.with_label_values(&[&card.name]).set(value); + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1062714..db56aef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,151 +1,56 @@ -/// This program utilizes several external libraries to perform its functions, such as dotenv, serde, tokio, etc. use dotenv; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, env, fs::File, fs::OpenOptions, io::BufReader}; +use std::{env, fs::File, fs::OpenOptions, io::BufReader, sync::Arc,net::SocketAddr}; use tokio::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; -use axum::{routing::get, Router}; -use hyper::{Server, http, body}; -use std::{net::SocketAddr, sync::Arc}; -use prometheus::{Encoder, TextEncoder, GaugeVec, Opts, Registry}; +use prometheus::Registry; -/// This structure defines a Card object, consisting of a name and a hashmap of prices. -#[derive(Serialize, Deserialize, Debug, Clone)] -struct Card { - name: String, - prices: HashMap>, -} +mod server; +mod card; -/// This structure defines a CardFromFile object, consisting of a name, count, and a USD value. -#[derive(Serialize, Deserialize, Debug, Clone)] -struct CardFromFile { - name: String, - count: usize, - usd_value: Option, -} - -/// This structure defines a CardFile object, consisting of a vector of CardFromFile objects. -#[derive(Serialize, Deserialize, Debug)] -struct CardFile { - cards: Vec, -} - -lazy_static::lazy_static! { - static ref CARD_VALUES: GaugeVec = GaugeVec::new( - Opts::new("card_value", "The value of cards"), - &["name"] - ).unwrap(); -} +use server::run_metrics_server; +use card::{CARD_VALUES, CardFile, process_card, process_top_cards}; #[tokio::main] async fn main() -> Result<(), Box> { dotenv::dotenv().ok(); + + let update_interval = dotenv::var("UPDATE_INTERVAL") + .map_err(|_| "UPDATE_INTERVAL is not defined in the .env file")? + .parse::() + .map_err(|_| "UPDATE_INTERVAL is not a valid number")? + * 3600; - // Retrieve the update interval from the .env file or return an error if not present. - let update_interval_str = match dotenv::var("UPDATE_INTERVAL") { - Ok(val) => val, - Err(_) => { - eprintln!("UPDATE_INTERVAL is not defined in the .env file"); - return Err("UPDATE_INTERVAL is not defined in the .env file".into()); - } - }; - // Attempt to parse the update interval as a u64 or return an error if it fails. - let update_interval = match update_interval_str.parse::() { - Ok(val) => val, - Err(_) => { - eprintln!("UPDATE_INTERVAL is not a valid number"); - return Err("UPDATE_INTERVAL is not a valid number".into()); - } - }; - - let update_interval = std::time::Duration::from_secs(update_interval * 3600); - - // Prometheus registry to register our metrics let registry = Arc::new(Registry::new()); - - // Register our metric with the registry registry.register(Box::new(CARD_VALUES.clone())).unwrap(); - // This is the address where we will expose the Prometheus /metrics endpoint let metrics_addr = SocketAddr::from(([127, 0, 0, 1], 3001)); - - // Spawn a new independent Tokio task for the metrics server tokio::spawn(run_metrics_server(metrics_addr, registry.clone())); - let mut interval = tokio::time::interval(update_interval); + let mut interval = tokio::time::interval(Duration::from_secs(update_interval)); loop { interval.tick().await; - // Retrieve the file path from the program's arguments or return an error if not present. let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("Please provide the path to the JSON file as an argument."); - return Ok(()); + 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)?; - // Setting up a progress bar for visual representation of the card processing progress. - let pb = ProgressBar::new(cards_data.cards.len() as u64); - let style = ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") - .unwrap(); - let style = style.progress_chars("#>-"); - pb.set_style(style); - - // For each card in the input file, retrieve the current price information and update the local data if necessary. + let pb = setup_progress_bar(cards_data.cards.len() as u64); for card_from_file in &mut cards_data.cards { - let request_url = format!( - "https://api.scryfall.com/cards/named?exact={}", - card_from_file.name - ); - let response = reqwest::get(&request_url).await?; - - let card: Card = response.json().await?; - - if let Some(price) = card.prices.get("usd") { - if let Some(price_str) = price { - card_from_file.usd_value = Some(price_str.clone()); - - // Assume price_str can be parsed to a f64 - let value = price_str.parse::().unwrap(); - - // Always update the metrics for this card - CARD_VALUES.with_label_values(&[&card_from_file.name]).set(value); - } else { - // If price is null, set it as 0 - card_from_file.usd_value = Some("0.0".to_string()); - CARD_VALUES.with_label_values(&[&card_from_file.name]).set(0.0); - } - } - - // Increment the progress bar and pause for a brief period. + process_card(card_from_file).await?; pb.inc(1); tokio::time::sleep(Duration::from_millis(100)).await; } - // After updating the card values, keep the top 10 highest value cards - cards_data.cards.sort_by(|a, b| b.usd_value.partial_cmp(&a.usd_value).unwrap()); - let top_10_cards = &cards_data.cards[..10]; - - // Update the metrics for the top 10 cards - for card in top_10_cards { - if let Some(price_str) = &card.usd_value { - let value = price_str.parse::().unwrap(); - - // Update the metrics for this card - CARD_VALUES.with_label_values(&[&card.name]).set(value); - } - } - - // Mark the progress bar as completed. + process_top_cards(&mut cards_data.cards); pb.finish_with_message("Completed!"); - // Write the updated card data back to the input file. let file = OpenOptions::new() .write(true) .truncate(true) @@ -155,26 +60,12 @@ async fn main() -> Result<(), Box> { } } -async fn run_metrics_server(addr: SocketAddr, registry: Arc) { - let app = Router::new().route("/metrics", get(move || { - let registry = Arc::clone(®istry); - 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(); -} - - +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 +} \ No newline at end of file diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..e9bdf51 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,24 @@ +use axum::{routing::get, Router}; +use hyper::{Server, http, body}; +use std::net::SocketAddr; +use prometheus::{Encoder, TextEncoder, Registry}; +use std::sync::Arc; + +pub async fn run_metrics_server(addr: SocketAddr, registry: Arc) { + let app = Router::new().route("/metrics", get(move || { + let registry = Arc::clone(®istry); + 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(); +}