From df7f2fbef48e70ccad356773e764a1f0814b6b23 Mon Sep 17 00:00:00 2001 From: Amir Hossein Habibi Date: Fri, 10 Oct 2025 14:46:46 +0330 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat(data):=20update=20sync=5Fa?= =?UTF-8?q?t=20to=20use=20Tehran=20timezone=20for=20timestamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/coin_ir/data.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/coin_ir/data.rs b/src/services/coin_ir/data.rs index b758c20..784ad9c 100644 --- a/src/services/coin_ir/data.rs +++ b/src/services/coin_ir/data.rs @@ -1,5 +1,6 @@ use crate::services::coin_ir::de_num_opt; use chrono::Utc; +use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -40,13 +41,14 @@ pub struct Quote { impl From<&HashMap> for CoinData { fn from(m: &HashMap) -> Self { let coin_info = Self::coin_info(m); + let tz: Tz = "Asia/Tehran".parse().unwrap_or(chrono_tz::UTC); Self { emami: Some(coin_info("sekee")), bahar: Some(coin_info("sekeb")), nim: Some(coin_info("nim")), rob: Some(coin_info("rob")), gerami: Some(coin_info("gerami")), - sync_at: Some(Utc::now().to_rfc3339()), + sync_at: Some(Utc::now().with_timezone(&tz).to_rfc3339()), } } } From 805f1e599f4b94a08664a5f324838100bcc6790f Mon Sep 17 00:00:00 2001 From: Amir Hossein Habibi Date: Fri, 10 Oct 2025 23:20:18 +0330 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(state):=20Imp?= =?UTF-8?q?rove=20structure=20and=20data=20flow=20=F0=9F=92=B1=20feat(api)?= =?UTF-8?q?:=20Add=20currency=20exchange=20&=20crypto=20price=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored state management for better consistency and scalability Added API for Iran currency exchange rates Added API for cryptocurrency prices --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app.rs | 13 ++- src/jobs/coin_sync.rs | 36 ------- src/jobs/mod.rs | 2 +- src/jobs/tgju_sync.rs | 36 +++++++ src/main.rs | 1 + src/request/data.rs | 0 src/request/mod.rs | 3 + .../request.rs => request/request_handler.rs} | 17 +-- src/request/tgju_data.rs | 100 ++++++++++++++++++ src/services/coin_ir/data.rs | 68 ------------ src/services/coin_ir/mod.rs | 4 - src/services/coin_ir/response.rs | 12 ++- src/services/crypto/mod.rs | 8 ++ src/services/crypto/response.rs | 23 ++++ src/services/currency/mod.rs | 8 ++ src/services/currency/response.rs | 21 ++++ src/services/gold_ir/mod.rs | 8 ++ src/services/gold_ir/response.rs | 20 ++++ src/services/health/response.rs | 32 +++++- src/services/mod.rs | 4 + src/services/routes.rs | 5 +- src/state/data.rs | 6 +- src/state/struct_state.rs | 29 +++-- .../helper.rs => utility/de_num_opt.rs} | 2 +- src/utility/mod.rs | 2 + templates/index.html | 23 +++- 28 files changed, 348 insertions(+), 139 deletions(-) delete mode 100644 src/jobs/coin_sync.rs create mode 100644 src/jobs/tgju_sync.rs create mode 100644 src/request/data.rs create mode 100644 src/request/mod.rs rename src/{services/coin_ir/request.rs => request/request_handler.rs} (70%) create mode 100644 src/request/tgju_data.rs delete mode 100644 src/services/coin_ir/data.rs create mode 100644 src/services/crypto/mod.rs create mode 100644 src/services/crypto/response.rs create mode 100644 src/services/currency/mod.rs create mode 100644 src/services/currency/response.rs create mode 100644 src/services/gold_ir/mod.rs create mode 100644 src/services/gold_ir/response.rs rename src/{services/coin_ir/helper.rs => utility/de_num_opt.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 2366bad..6f696d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2471,7 +2471,7 @@ dependencies = [ [[package]] name = "rust_rest_api" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anyhow", "askama", diff --git a/Cargo.toml b/Cargo.toml index 7cc62f8..b918cdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust_rest_api" -version = "0.3.0" +version = "0.4.0" edition = "2024" license = "MIT" authors = ["Habibi-Dev"] diff --git a/src/app.rs b/src/app.rs index 5262669..2065f09 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use crate::core::config::Config; -use crate::jobs::{cache_flush::CacheFlushJob, coin_sync::CoinSyncJob}; +use crate::jobs::{cache_flush::CacheFlushJob}; use crate::server; use crate::services::{cache::StatsCache, routes::Routes}; use crate::state::{self, APP_STATE, AppState}; @@ -7,6 +7,7 @@ use anyhow::{Context, Result}; use migration::{Migrator, MigratorTrait}; use sea_orm::Database; use std::sync::Arc; +use crate::jobs::tgju_sync::TgjuSyncJob; pub async fn run() -> Result<()> { let config = load_config()?; @@ -48,9 +49,15 @@ async fn setup_database() -> Result { fn start_background_jobs(cache: &Arc) -> Result<()> { let flush_job = CacheFlushJob::new(Arc::clone(cache)); - let coin_job = CoinSyncJob::new(); + let tgju_job = TgjuSyncJob::new(); - crate::services::jobs::FlushJob::start(vec![flush_job.into_task(), coin_job.into_task()], None); + crate::services::jobs::FlushJob::start( + vec![ + flush_job.into_task(), + tgju_job.into_task(), + ], + None, + ); Ok(()) } diff --git a/src/jobs/coin_sync.rs b/src/jobs/coin_sync.rs deleted file mode 100644 index 4252707..0000000 --- a/src/jobs/coin_sync.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::jobs::JobTask; -use crate::services::coin_ir::request::RequestCoinIr; -use crate::state::APP_STATE; -use std::sync::Arc; - -pub struct CoinSyncJob; - -impl CoinSyncJob { - pub fn new() -> Self { - Self - } - - pub fn into_task(self) -> JobTask { - Arc::new(|| { - Box::pin(async { - if let Err(e) = sync_coins().await { - eprintln!("Coin sync job failed: {}", e); - } - }) - }) - } -} - -async fn sync_coins() -> Result<(), Box> { - let service = RequestCoinIr::new(); - let coins = service.coins_typed().await?; - - let state = APP_STATE.get().ok_or("Application state not available")?; - - state - .coin_tx - .send(coins) - .map_err(|_| "Failed to send coins to channel")?; - - Ok(()) -} diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 5fa1e77..5bb3929 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -1,5 +1,5 @@ pub mod cache_flush; -pub mod coin_sync; +pub mod tgju_sync; use std::{future::Future, pin::Pin, sync::Arc}; diff --git a/src/jobs/tgju_sync.rs b/src/jobs/tgju_sync.rs new file mode 100644 index 0000000..ffa7d47 --- /dev/null +++ b/src/jobs/tgju_sync.rs @@ -0,0 +1,36 @@ +use crate::jobs::JobTask; +use crate::request::request_handler; +use crate::state::APP_STATE; +use std::sync::Arc; + +pub struct TgjuSyncJob; + +impl TgjuSyncJob { + pub fn new() -> Self { + Self + } + + pub fn into_task(self) -> JobTask { + Arc::new(|| { + Box::pin(async { + if let Err(e) = tgju_sync().await { + eprintln!("Gold sync job failed: {}", e); + } + }) + }) + } +} + +async fn tgju_sync() -> Result<(), Box> { + let service = request_handler::RequestHandler::new(); + let tg = service.tgju_typed().await?; + + let state = APP_STATE.get().ok_or("Application state not available")?; + + state + .financial_rates_tx + .send(tg) + .map_err(|_| "Failed to send golds to channel")?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 87e293b..ec2fe22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod server; mod services; mod state; mod utility; +mod request; #[tokio::main] async fn main() { diff --git a/src/request/data.rs b/src/request/data.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/request/mod.rs b/src/request/mod.rs new file mode 100644 index 0000000..860b9df --- /dev/null +++ b/src/request/mod.rs @@ -0,0 +1,3 @@ +pub mod request_handler; +pub mod tgju_data; +pub mod data; diff --git a/src/services/coin_ir/request.rs b/src/request/request_handler.rs similarity index 70% rename from src/services/coin_ir/request.rs rename to src/request/request_handler.rs index c76bd90..a83d3d8 100644 --- a/src/services/coin_ir/request.rs +++ b/src/request/request_handler.rs @@ -1,24 +1,25 @@ -use crate::services::coin_ir::data::{ApiResponse, CoinData}; use reqwest::Client; use uuid::Uuid; +use crate::request::tgju_data::{ApiResponse, TgjuData}; -pub struct RequestCoinIr { +pub struct RequestHandler{ client: Client, } -impl RequestCoinIr { +impl RequestHandler{ pub fn new() -> Self { let client = Client::builder() .use_rustls_tls() .timeout(std::time::Duration::from_secs(10)) .build() - .expect("reqwest client build failed"); + .expect("reqwest client [RequestGold] build failed"); Self { client } + } pub async fn start(&self) -> Result { let uuid = Uuid::new_v4(); - let url = format!("https://call1.tgju.org/ajax.json?rev={}", uuid); + let url = format!("https://call3.tgju.org/ajax.json?rev={}", uuid); let resp = self .client .get(url) @@ -31,8 +32,8 @@ impl RequestCoinIr { Ok(api) } - pub async fn coins_typed(&self) -> Result { + pub async fn tgju_typed(&self) -> Result { let api = self.start().await?; - Ok(CoinData::from(&api.current)) + Ok(TgjuData::from(&api.current)) } -} +} \ No newline at end of file diff --git a/src/request/tgju_data.rs b/src/request/tgju_data.rs new file mode 100644 index 0000000..d8d0ebd --- /dev/null +++ b/src/request/tgju_data.rs @@ -0,0 +1,100 @@ +use crate::utility::de_num_opt; +use chrono::Utc; +use chrono_tz::Tz; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize)] +pub struct Info { + pub current: Option, + pub highest: Option, + pub lowest: Option, + pub updated_at: Option, +} +#[derive(Debug, Clone, Serialize)] +pub struct TgjuData { + pub gold_18_750: Option, + pub gold_18_740: Option, + pub gold_24: Option, + pub mesghal: Option, + pub used_gold: Option, + pub emami: Option, + pub bahar: Option, + pub nim: Option, + pub rob: Option, + pub gerami: Option, + pub price_dollar_rl: Option, + pub price_eur: Option, + pub price_aed: Option, + pub price_gbp: Option, + pub price_try: Option, + pub crypto_bitcoin_irr: Option, + pub crypto_ethereum_irr: Option, + pub crypto_binance_coin_irr: Option, + pub crypto_ripple_irr: Option, + pub crypto_usd_coin_irr: Option, + pub crypto_dogecoin_irr: Option, + pub crypto_tron_irr: Option, + pub sync_at: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ApiResponse { + pub current: HashMap, +} +#[derive(Debug, Deserialize, Clone)] +pub struct Quote { + #[serde(deserialize_with = "de_num_opt")] + pub p: Option, + #[serde(deserialize_with = "de_num_opt")] + pub h: Option, + #[serde(deserialize_with = "de_num_opt")] + pub l: Option, + pub ts: Option, +} + +impl From<&HashMap> for TgjuData { + fn from(m: &HashMap) -> Self { + let info = Self::info(m); + let tz: Tz = "Asia/Tehran".parse().unwrap_or(chrono_tz::UTC); + Self { + gold_18_750: Some(info("geram18")), + gold_18_740: Some(info("gold_740k")), + gold_24: Some(info("geram24")), + mesghal: Some(info("mesghal")), + used_gold: Some(info("gold_mini_size")), + emami: Some(info("sekee")), + bahar: Some(info("sekeb")), + nim: Some(info("nim")), + rob: Some(info("rob")), + gerami: Some(info("gerami")), + price_dollar_rl: Some(info("price_dollar_rl")), + price_eur: Some(info("price_eur")), + price_aed: Some(info("price_aed")), + price_gbp: Some(info("price_gbp")), + price_try: Some(info("price_try")), + crypto_bitcoin_irr: Some(info("crypto-bitcoin-irr")), + crypto_ethereum_irr: Some(info("crypto-ethereum-irr")), + crypto_binance_coin_irr: Some(info("crypto-binance-coin-irr")), + crypto_ripple_irr: Some(info("crypto-ripple-irr")), + crypto_usd_coin_irr: Some(info("crypto-usd-coin-irr")), + crypto_dogecoin_irr: Some(info("crypto-dogecoin-irr")), + crypto_tron_irr: Some(info("crypto-tron-irr")), + sync_at: Some(Utc::now().with_timezone(&tz).to_rfc3339()), + } + } +} + +impl TgjuData { + pub fn info<'a>(m: &'a HashMap) -> impl Fn(&str) -> Info + 'a { + move |k: &str| { + let q = m.get(k); + Info { + current: q.and_then(|q| q.p), + highest: q.and_then(|q| q.h), + lowest: q.and_then(|q| q.l), + updated_at: q.and_then(|q| q.ts.clone()), + } + } + } +} diff --git a/src/services/coin_ir/data.rs b/src/services/coin_ir/data.rs deleted file mode 100644 index 784ad9c..0000000 --- a/src/services/coin_ir/data.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::services::coin_ir::de_num_opt; -use chrono::Utc; -use chrono_tz::Tz; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Debug, Clone, Serialize)] -pub struct CoinInfo { - pub current: Option, - pub highest: Option, - pub lowest: Option, - pub updated_at: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct CoinData { - pub emami: Option, - pub bahar: Option, - pub nim: Option, - pub rob: Option, - pub gerami: Option, - pub sync_at: Option, -} - -#[derive(Debug, Deserialize)] -pub struct ApiResponse { - pub current: HashMap, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct Quote { - #[serde(deserialize_with = "de_num_opt")] - pub p: Option, - #[serde(deserialize_with = "de_num_opt")] - pub h: Option, - #[serde(deserialize_with = "de_num_opt")] - pub l: Option, - pub ts: Option, -} - -impl From<&HashMap> for CoinData { - fn from(m: &HashMap) -> Self { - let coin_info = Self::coin_info(m); - let tz: Tz = "Asia/Tehran".parse().unwrap_or(chrono_tz::UTC); - Self { - emami: Some(coin_info("sekee")), - bahar: Some(coin_info("sekeb")), - nim: Some(coin_info("nim")), - rob: Some(coin_info("rob")), - gerami: Some(coin_info("gerami")), - sync_at: Some(Utc::now().with_timezone(&tz).to_rfc3339()), - } - } -} - -impl CoinData { - pub fn coin_info<'a>(m: &'a HashMap) -> impl Fn(&str) -> CoinInfo + 'a { - move |k: &str| { - let q = m.get(k); - CoinInfo { - current: q.and_then(|q| q.p), - highest: q.and_then(|q| q.h), - lowest: q.and_then(|q| q.l), - updated_at: q.and_then(|q| q.ts.clone()), - } - } - } -} diff --git a/src/services/coin_ir/mod.rs b/src/services/coin_ir/mod.rs index 96be460..2d679f7 100644 --- a/src/services/coin_ir/mod.rs +++ b/src/services/coin_ir/mod.rs @@ -1,11 +1,7 @@ -pub(crate) mod data; -pub mod helper; -pub mod request; mod response; use crate::services::coin_ir::response::response_coin; use axum::routing::{MethodRouter, get}; -pub use helper::de_num_opt; pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { Vec::from([("/ir", get(response_coin))]) diff --git a/src/services/coin_ir/response.rs b/src/services/coin_ir/response.rs index 0f4f99b..1b0e674 100644 --- a/src/services/coin_ir/response.rs +++ b/src/services/coin_ir/response.rs @@ -5,7 +5,17 @@ use serde_json::json; pub async fn response_coin() -> Json> { let state = APP_STATE.get(); - let payload = json!(state.unwrap().coin_rx.borrow().clone()); + let data = state.unwrap().financial_rates_rx.borrow().clone(); + + let payload = json!({ + "emami": data.emami, + "gerami": data.gerami, + "bahar": data.bahar, + "nim": data.nim, + "rob": data.rob, + "source": "tgju.org", + "sync_at": data.sync_at, + }); Json(ApiResponse::success(payload)) } diff --git a/src/services/crypto/mod.rs b/src/services/crypto/mod.rs new file mode 100644 index 0000000..13a025f --- /dev/null +++ b/src/services/crypto/mod.rs @@ -0,0 +1,8 @@ +mod response; + +use axum::routing::{MethodRouter, get}; +use crate::services::crypto::response::response_crypto_ir; + +pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { + Vec::from([("/ir", get(response_crypto_ir))]) +} diff --git a/src/services/crypto/response.rs b/src/services/crypto/response.rs new file mode 100644 index 0000000..d3695c4 --- /dev/null +++ b/src/services/crypto/response.rs @@ -0,0 +1,23 @@ +use crate::core::response::ApiResponse; +use crate::state::APP_STATE; +use axum::Json; +use serde_json::json; + +pub async fn response_crypto_ir() -> Json> { + let state = APP_STATE.get(); + let data = state.unwrap().financial_rates_rx.borrow().clone(); + + let payload = json!({ + "bitcoin": data.crypto_bitcoin_irr, + "ethereum": data.crypto_ethereum_irr, + "binance": data.crypto_binance_coin_irr, + "ripple": data.crypto_ripple_irr, + "usd": data.crypto_usd_coin_irr, + "dogecoin": data.crypto_dogecoin_irr, + "tron": data.crypto_tron_irr, + "source": "tgju.org", + "sync_at": data.sync_at, + }); + + Json(ApiResponse::success(payload)) +} diff --git a/src/services/currency/mod.rs b/src/services/currency/mod.rs new file mode 100644 index 0000000..e6c9156 --- /dev/null +++ b/src/services/currency/mod.rs @@ -0,0 +1,8 @@ +mod response; + +use axum::routing::{MethodRouter, get}; +use crate::services::currency::response::response_currency_ir; + +pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { + Vec::from([("/ir", get(response_currency_ir))]) +} diff --git a/src/services/currency/response.rs b/src/services/currency/response.rs new file mode 100644 index 0000000..98dbbf6 --- /dev/null +++ b/src/services/currency/response.rs @@ -0,0 +1,21 @@ +use crate::core::response::ApiResponse; +use crate::state::APP_STATE; +use axum::Json; +use serde_json::json; + +pub async fn response_currency_ir() -> Json> { + let state = APP_STATE.get(); + let data = state.unwrap().financial_rates_rx.borrow().clone(); + + let payload = json!({ + "dollar": data.price_dollar_rl, + "eur": data.price_eur, + "aed": data.price_aed, + "gbp": data.price_gbp, + "try": data.price_try, + "source": "tgju.org", + "sync_at": data.sync_at, + }); + + Json(ApiResponse::success(payload)) +} diff --git a/src/services/gold_ir/mod.rs b/src/services/gold_ir/mod.rs new file mode 100644 index 0000000..446f3a3 --- /dev/null +++ b/src/services/gold_ir/mod.rs @@ -0,0 +1,8 @@ +mod response; + +use crate::services::gold_ir::response::response_gold; +use axum::routing::{MethodRouter, get}; + +pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { + Vec::from([("/ir", get(response_gold))]) +} diff --git a/src/services/gold_ir/response.rs b/src/services/gold_ir/response.rs new file mode 100644 index 0000000..1f37f22 --- /dev/null +++ b/src/services/gold_ir/response.rs @@ -0,0 +1,20 @@ +use crate::core::response::ApiResponse; +use crate::state::APP_STATE; +use axum::Json; +use serde_json::json; + +pub async fn response_gold() -> Json> { + let state = APP_STATE.get(); + let data = state.unwrap().financial_rates_rx.borrow().clone(); + let payload = json!({ + "gold_18_750": data.gold_18_750, + "gold_18_740": data.gold_18_740, + "gold_24": data.gold_24, + "mesghal": data.mesghal, + "used_gold": data.used_gold, + "source": "tgju.org", + "sync_at": data.sync_at, + }); + + Json(ApiResponse::success(payload)) +} diff --git a/src/services/health/response.rs b/src/services/health/response.rs index 1506ab3..93c17e6 100644 --- a/src/services/health/response.rs +++ b/src/services/health/response.rs @@ -45,7 +45,7 @@ pub async fn health_check() -> api_response::Json pub async fn collect_endpoint_stats(state: &AppState) -> EndpointStats { let total = state.stats_cache.total_hits().await; - let (health, time, tehran, ip, country, country_full, coin_ir) = tokio::join!( + let (health, time, tehran, ip, country, country_full, coin_ir, gold_ir, currency_ir, crypto_ir) = tokio::join!( state.stats_cache.get("/health"), state.stats_cache.get("/api/v1/time"), state.stats_cache.get("/api/v1/time/asia/tehran"), @@ -53,6 +53,9 @@ pub async fn collect_endpoint_stats(state: &AppState) -> EndpointStats { state.stats_cache.get("/api/v1/country"), state.stats_cache.get("/api/v1/country/full"), state.stats_cache.get("/api/v1/coin/ir"), + state.stats_cache.get("/api/v1/gold/ir"), + state.stats_cache.get("/api/v1/currency/ir"), + state.stats_cache.get("/api/v1/crypto/ir"), ); EndpointStats { @@ -64,6 +67,9 @@ pub async fn collect_endpoint_stats(state: &AppState) -> EndpointStats { country, country_full, coin_ir, + gold_ir, + currency_ir, + crypto_ir, } } @@ -128,9 +134,24 @@ fn build_endpoints_list() -> serde_json::Value { "example": url("/api/v1/country/full") }, { - "path": "/country/full", - "desc": "Explore countries, currencies, languages & flags 🗺️", - "example": url("/api/v1/country/full") + "path": "/coin/ir", + "desc": "Get live retail prices for Iranian gold coins — including full, half, quarter, and one-gram (Gerami) coins 🪙💰", + "example": url("/api/v1/coin/ir") + }, + { + "path": "/gold/ir", + "desc": "Get live retail prices for 18K and 24K gold, second-hand gold, and gold by Mesghal (gram weight unit) in Iran. 🪙✨", + "example": url("/api/v1/gold/ir") + }, + { + "path": "/crypto", + "desc": "Get real-time cryptocurrency market prices for major digital assets like Bitcoin, Ethereum, and more. 💎📊", + "example": url("/api/v1/crypto/ir") + }, + { + "path": "/currency/ir", + "desc": "Access live exchange rates for major currencies such as USD, EUR, and GBP against the Iranian Rial. 💵🇮🇷", + "example": url("/api/v1/currency/ir") } ]) } @@ -144,4 +165,7 @@ pub struct EndpointStats { pub country: Option, pub country_full: Option, pub coin_ir: Option, + pub gold_ir: Option, + pub currency_ir: Option, + pub crypto_ir: Option, } diff --git a/src/services/mod.rs b/src/services/mod.rs index 06cb047..7dfafaf 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -7,4 +7,8 @@ mod ip; pub mod jobs; pub mod routes; mod time; +pub mod gold_ir; +pub mod currency; +pub mod crypto; + pub use cache::StatsCache; diff --git a/src/services/routes.rs b/src/services/routes.rs index c250041..3de18dd 100644 --- a/src/services/routes.rs +++ b/src/services/routes.rs @@ -1,6 +1,6 @@ use crate::middleware::visit_event; use crate::services::index::index_handler; -use crate::services::{coin_ir, country, health, ip, time}; +use crate::services::{coin_ir, country, crypto, currency, gold_ir, health, ip, time}; use crate::state::AppState; use axum::extract::Path; use axum::http::{StatusCode, header}; @@ -27,6 +27,9 @@ impl Routes { .nest("/ip", Self::generate(ip::routers_list())) .nest("/country", Self::generate(country::routers_list())) .nest("/coin", Self::generate(coin_ir::routers_list())) + .nest("/gold", Self::generate(gold_ir::routers_list())) + .nest("/crypto", Self::generate(crypto::routers_list())) + .nest("/currency", Self::generate(currency::routers_list())) .route("/flags/{code}", get(country::flag::get_flag)); Router::new() diff --git a/src/state/data.rs b/src/state/data.rs index fd9d6ff..92cbc41 100644 --- a/src/state/data.rs +++ b/src/state/data.rs @@ -1,15 +1,15 @@ use crate::services::StatsCache; -use crate::services::coin_ir::data::CoinData; use sea_orm::DatabaseConnection; use std::sync::Arc; use std::time::Instant; use tokio::sync::watch; +use crate::request::tgju_data::TgjuData; #[derive(Clone)] pub struct AppState { pub _db: DatabaseConnection, pub stats_cache: Arc, - pub coin_tx: watch::Sender, - pub coin_rx: watch::Receiver, + pub financial_rates_tx: watch::Sender, + pub financial_rates_rx: watch::Receiver, pub uptime: Instant, } diff --git a/src/state/struct_state.rs b/src/state/struct_state.rs index 523d685..d0b5876 100644 --- a/src/state/struct_state.rs +++ b/src/state/struct_state.rs @@ -1,5 +1,5 @@ +use crate::request::tgju_data::TgjuData; use crate::services::StatsCache; -use crate::services::coin_ir::data::CoinData; use crate::state::AppState; use sea_orm::DatabaseConnection; use std::sync::{Arc, OnceLock}; @@ -12,27 +12,44 @@ pub struct State; impl State { pub fn init(db: DatabaseConnection, cache: Arc) { - let (tx, rx) = Self::state_coin(); + let (tx, rx) = Self::financial_coin(); APP_STATE .set(AppState { _db: db, stats_cache: cache, - coin_tx: tx, - coin_rx: rx, + financial_rates_tx: tx, + financial_rates_rx: rx, uptime: Instant::now(), }) .ok(); } - fn state_coin() -> (Sender, Receiver) { - let (tx, rx) = watch::channel(CoinData { + fn financial_coin() -> (Sender, Receiver) { + let (tx, rx) = watch::channel(TgjuData { + gold_18_750: None, + gold_18_740: None, + gold_24: None, + mesghal: None, gerami: None, + price_dollar_rl: None, + price_eur: None, + price_aed: None, + price_gbp: None, + price_try: None, + crypto_bitcoin_irr: None, + crypto_ethereum_irr: None, + crypto_binance_coin_irr: None, + crypto_ripple_irr: None, + crypto_usd_coin_irr: None, + crypto_dogecoin_irr: None, emami: None, bahar: None, nim: None, rob: None, sync_at: None, + used_gold: None, + crypto_tron_irr: None, }); (tx, rx) } diff --git a/src/services/coin_ir/helper.rs b/src/utility/de_num_opt.rs similarity index 100% rename from src/services/coin_ir/helper.rs rename to src/utility/de_num_opt.rs index 39b0553..9f1768d 100644 --- a/src/services/coin_ir/helper.rs +++ b/src/utility/de_num_opt.rs @@ -1,5 +1,5 @@ -use crate::utility::clean_num; use serde::Deserialize; +use crate::utility::clean_num; pub fn de_num_opt<'de, D>(d: D) -> Result, D::Error> where diff --git a/src/utility/mod.rs b/src/utility/mod.rs index 308d488..ee5544d 100644 --- a/src/utility/mod.rs +++ b/src/utility/mod.rs @@ -1,7 +1,9 @@ pub mod clean_num; pub mod fa_to_en_digits; pub mod url; +pub mod de_num_opt; pub use clean_num::clean_num; pub use fa_to_en_digits::fa_to_en_digits; pub use url::url; +pub use de_num_opt::de_num_opt; diff --git a/templates/index.html b/templates/index.html index 8578c31..e38c80a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -50,6 +50,24 @@ "path": "/coin/ir", "desc": "Get live retail prices for Iranian gold coins — including full, half, quarter, and one-gram (Gerami) coins 🪙💰", "example": "{{ url }}api/v1/coin/ir" + }, + { + "title": "Gold Prices (Iran)", + "path": "/gold/ir", + "desc": "Get live retail prices for 18K and 24K gold, second-hand gold, and gold by Mesghal (gram weight unit) in Iran. 🪙✨", + "example": "{{ url }}api/v1/gold/ir" + }, + { + "title": "Cryptocurrency Prices", + "path": "/crypto/ir", + "desc": "Get real-time cryptocurrency market prices for major digital assets like Bitcoin, Ethereum, and more. 💎📊", + "example": "{{ url }}api/v1/crypto/ir" + }, + { + "title": "Currency Exchange Rates (Iran)", + "path": "/currency/ir", + "desc": "Access live exchange rates for major currencies such as USD, EUR, and GBP against the Iranian Rial. 💵🇮🇷", + "example": "{{ url }}api/v1/currency/ir" } ], usage: { @@ -60,7 +78,10 @@ "/ip": "{{ stats.ip.unwrap_or(0) }}", "/country": "{{ stats.country.unwrap_or(0) }}", "/country/full": "{{ stats.country_full.unwrap_or(0) }}", - "/coin/ir": "{{ stats.coin_ir.unwrap_or(0) }}" + "/coin/ir": "{{ stats.coin_ir.unwrap_or(0) }}", + "/gold/ir": "{{ stats.gold_ir.unwrap_or(0) }}", + "/currency/ir": "{{ stats.currency_ir.unwrap_or(0) }}", + "/crypto/ir": "{{ stats.crypto_ir.unwrap_or(0) }}", } }, privacy: "We log minimal request metadata for abuse prevention and aggregate analytics.", From 0a957ea97825b3c8cb5df241075ba49b9c603633 Mon Sep 17 00:00:00 2001 From: Amir Hossein Habibi Date: Fri, 10 Oct 2025 23:20:34 +0330 Subject: [PATCH 3/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(state):=20Imp?= =?UTF-8?q?rove=20structure=20and=20data=20flow=20=F0=9F=92=B1=20feat(api)?= =?UTF-8?q?:=20Add=20currency=20exchange=20&=20crypto=20price=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored state management for better consistency and scalability Added API for Iran currency exchange rates Added API for cryptocurrency prices --- src/app.rs | 12 +++--------- src/main.rs | 2 +- src/request/data.rs | 1 + src/request/mod.rs | 2 +- src/request/request_handler.rs | 9 ++++----- src/services/crypto/mod.rs | 2 +- src/services/currency/mod.rs | 2 +- src/services/mod.rs | 6 +++--- src/state/data.rs | 2 +- src/utility/de_num_opt.rs | 2 +- src/utility/mod.rs | 4 ++-- 11 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2065f09..476325e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::core::config::Config; -use crate::jobs::{cache_flush::CacheFlushJob}; +use crate::jobs::cache_flush::CacheFlushJob; +use crate::jobs::tgju_sync::TgjuSyncJob; use crate::server; use crate::services::{cache::StatsCache, routes::Routes}; use crate::state::{self, APP_STATE, AppState}; @@ -7,7 +8,6 @@ use anyhow::{Context, Result}; use migration::{Migrator, MigratorTrait}; use sea_orm::Database; use std::sync::Arc; -use crate::jobs::tgju_sync::TgjuSyncJob; pub async fn run() -> Result<()> { let config = load_config()?; @@ -51,13 +51,7 @@ fn start_background_jobs(cache: &Arc) -> Result<()> { let flush_job = CacheFlushJob::new(Arc::clone(cache)); let tgju_job = TgjuSyncJob::new(); - crate::services::jobs::FlushJob::start( - vec![ - flush_job.into_task(), - tgju_job.into_task(), - ], - None, - ); + crate::services::jobs::FlushJob::start(vec![flush_job.into_task(), tgju_job.into_task()], None); Ok(()) } diff --git a/src/main.rs b/src/main.rs index ec2fe22..261ad0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,11 @@ mod entities; mod jobs; mod middleware; mod repository; +mod request; mod server; mod services; mod state; mod utility; -mod request; #[tokio::main] async fn main() { diff --git a/src/request/data.rs b/src/request/data.rs index e69de29..8b13789 100644 --- a/src/request/data.rs +++ b/src/request/data.rs @@ -0,0 +1 @@ + diff --git a/src/request/mod.rs b/src/request/mod.rs index 860b9df..d3040a8 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -1,3 +1,3 @@ +pub mod data; pub mod request_handler; pub mod tgju_data; -pub mod data; diff --git a/src/request/request_handler.rs b/src/request/request_handler.rs index a83d3d8..d3cce8c 100644 --- a/src/request/request_handler.rs +++ b/src/request/request_handler.rs @@ -1,12 +1,12 @@ +use crate::request::tgju_data::{ApiResponse, TgjuData}; use reqwest::Client; use uuid::Uuid; -use crate::request::tgju_data::{ApiResponse, TgjuData}; -pub struct RequestHandler{ +pub struct RequestHandler { client: Client, } -impl RequestHandler{ +impl RequestHandler { pub fn new() -> Self { let client = Client::builder() .use_rustls_tls() @@ -14,7 +14,6 @@ impl RequestHandler{ .build() .expect("reqwest client [RequestGold] build failed"); Self { client } - } pub async fn start(&self) -> Result { @@ -36,4 +35,4 @@ impl RequestHandler{ let api = self.start().await?; Ok(TgjuData::from(&api.current)) } -} \ No newline at end of file +} diff --git a/src/services/crypto/mod.rs b/src/services/crypto/mod.rs index 13a025f..705ae33 100644 --- a/src/services/crypto/mod.rs +++ b/src/services/crypto/mod.rs @@ -1,7 +1,7 @@ mod response; -use axum::routing::{MethodRouter, get}; use crate::services::crypto::response::response_crypto_ir; +use axum::routing::{MethodRouter, get}; pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { Vec::from([("/ir", get(response_crypto_ir))]) diff --git a/src/services/currency/mod.rs b/src/services/currency/mod.rs index e6c9156..f938440 100644 --- a/src/services/currency/mod.rs +++ b/src/services/currency/mod.rs @@ -1,7 +1,7 @@ mod response; -use axum::routing::{MethodRouter, get}; use crate::services::currency::response::response_currency_ir; +use axum::routing::{MethodRouter, get}; pub fn routers_list() -> Vec<(&'static str, MethodRouter)> { Vec::from([("/ir", get(response_currency_ir))]) diff --git a/src/services/mod.rs b/src/services/mod.rs index 7dfafaf..1827e4b 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,14 +1,14 @@ pub mod cache; pub(crate) mod coin_ir; mod country; +pub mod crypto; +pub mod currency; +pub mod gold_ir; mod health; mod index; mod ip; pub mod jobs; pub mod routes; mod time; -pub mod gold_ir; -pub mod currency; -pub mod crypto; pub use cache::StatsCache; diff --git a/src/state/data.rs b/src/state/data.rs index 92cbc41..542106f 100644 --- a/src/state/data.rs +++ b/src/state/data.rs @@ -1,9 +1,9 @@ +use crate::request::tgju_data::TgjuData; use crate::services::StatsCache; use sea_orm::DatabaseConnection; use std::sync::Arc; use std::time::Instant; use tokio::sync::watch; -use crate::request::tgju_data::TgjuData; #[derive(Clone)] pub struct AppState { diff --git a/src/utility/de_num_opt.rs b/src/utility/de_num_opt.rs index 9f1768d..39b0553 100644 --- a/src/utility/de_num_opt.rs +++ b/src/utility/de_num_opt.rs @@ -1,5 +1,5 @@ -use serde::Deserialize; use crate::utility::clean_num; +use serde::Deserialize; pub fn de_num_opt<'de, D>(d: D) -> Result, D::Error> where diff --git a/src/utility/mod.rs b/src/utility/mod.rs index ee5544d..348f796 100644 --- a/src/utility/mod.rs +++ b/src/utility/mod.rs @@ -1,9 +1,9 @@ pub mod clean_num; +pub mod de_num_opt; pub mod fa_to_en_digits; pub mod url; -pub mod de_num_opt; pub use clean_num::clean_num; +pub use de_num_opt::de_num_opt; pub use fa_to_en_digits::fa_to_en_digits; pub use url::url; -pub use de_num_opt::de_num_opt;