Skip to content

Instantly share code, notes, and snippets.

@jericbas
Last active February 18, 2026 00:35
Show Gist options
  • Select an option

  • Save jericbas/e63b7865140b67d767bc18a2009db870 to your computer and use it in GitHub Desktop.

Select an option

Save jericbas/e63b7865140b67d767bc18a2009db870 to your computer and use it in GitHub Desktop.
Rust CLI: Currency converter to PHP with 12h rate caching using FreeCurrencyAPI

Currency CLI

Converts currencies to PHP using FreeCurrencyAPI.

Setup

export FREE_CURRENCY_API_KEY=your_api_key_here

Usage

cargo run -- 100 USD
cargo run -- 50 EUR
cargo run -- 1000 JPY

Features

  • 12-hour caching: Rates are cached for up to 12 hours to reduce API calls
  • Cache location: ~/.cache/<CURRENCY>_.currency_cache.json
  • Override cache dir: Set CURRENCY_CACHE_DIR environment variable

Cache

First run fetches from API:

Fetching fresh rate from API...
100.00 USD = 5600.00 PHP
Rate: 1 USD = 56.0000 PHP

Subsequent runs within 12 hours use cache:

Using cached rate (12 hours max)
100.00 USD = 5600.00 PHP
Rate: 1 USD = 56.0000 PHP

Build Release

cargo build --release

Binary at target/release/currency-cli

[package]
name = "currency-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
FREE_CURRENCY_API_KEY=your_api_key_here
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const CACHE_DURATION_HOURS: u64 = 12;
const CACHE_FILENAME: &str = ".currency_cache.json";
#[derive(Deserialize, Serialize)]
struct CacheEntry {
base_currency: String,
php_rate: f64,
timestamp: u64,
}
#[derive(Deserialize)]
struct ApiResponse {
data: CurrencyData,
}
#[derive(Deserialize)]
struct CurrencyData {
#[serde(rename = "PHP")]
php: CurrencyInfo,
}
#[derive(Deserialize)]
struct CurrencyInfo {
value: f64,
}
fn get_cache_dir() -> PathBuf {
env::var("CURRENCY_CACHE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
env::var("HOME")
.map(PathBuf::from)
.unwrap_or_default()
.join(".cache")
})
}
fn get_cache_path(base_currency: &str) -> PathBuf {
get_cache_dir().join(format!("{}_{}", base_currency, CACHE_FILENAME))
}
fn is_cache_valid(entry: &CacheEntry) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age = now.saturating_sub(entry.timestamp);
age < Duration::from_secs(CACHE_DURATION_HOURS * 3600).as_secs()
}
async fn fetch_rate(base_currency: &str, api_key: &str) -> Result<f64> {
let url = format!(
"https://api.freecurrencyapi.com/v1/latest?apikey={}&base_currency={}&currencies=PHP",
api_key, base_currency
);
let client = reqwest::Client::new();
let response = client
.get(&url)
.send()
.await
.with_context(|| "Failed to fetch exchange rate")?;
let api_response: ApiResponse = response
.json()
.await
.with_context(|| "Failed to parse API response")?;
Ok(api_response.data.php.value)
}
fn load_cached_rate(base_currency: &str) -> Option<f64> {
let cache_path = get_cache_path(base_currency);
let contents = fs::read_to_string(&cache_path).ok()?;
let entry: CacheEntry = serde_json::from_str(&contents).ok()?;
if is_cache_valid(&entry) {
println!("Using cached rate ({} hours max)", CACHE_DURATION_HOURS);
Some(entry.php_rate)
} else {
None
}
}
fn save_cached_rate(base_currency: &str, rate: f64) -> Result<()> {
let cache_dir = get_cache_dir();
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)
.with_context(|| format!("Failed to create cache dir: {:?}", cache_dir))?;
}
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let entry = CacheEntry {
base_currency: base_currency.to_string(),
php_rate: rate,
timestamp,
};
let cache_path = get_cache_path(base_currency);
let contents = serde_json::to_string(&entry)
.with_context(|| "Failed to serialize cache entry")?;
fs::write(&cache_path, contents)
.with_context(|| format!("Failed to write cache: {:?}", cache_path))?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} <amount> <FROM_CURRENCY>", args[0]);
eprintln!("Example: {} 100 USD", args[0]);
eprintln!("Cache location: ~/.cache/<CURRENCY>_{}", CACHE_FILENAME);
eprintln!("Set CURRENCY_CACHE_DIR to override cache directory");
std::process::exit(1);
}
let amount: f64 = args[1]
.parse()
.with_context(|| format!("Invalid amount: {}", args[1]))?;
let from_currency = &args[2].to_uppercase();
// Try cache first
let rate = match load_cached_rate(from_currency) {
Some(rate) => rate,
None => {
println!("Fetching fresh rate from API...");
let api_key = env::var("FREE_CURRENCY_API_KEY")
.with_context(|| "FREE_CURRENCY_API_KEY not set")?;
let rate = fetch_rate(from_currency, &api_key).await?;
// Cache the result (best effort - don't fail on cache errors)
if let Err(e) = save_cached_rate(from_currency, rate) {
eprintln!("Warning: Failed to cache rate: {}", e);
}
rate
}
};
let result = amount * rate;
println!("{:.2} {} = {:.2} PHP", amount, from_currency, result);
println!("Rate: 1 {} = {:.4} PHP", from_currency, rate);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment