Initial commit
This commit is contained in:
commit
4efafdd85b
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "currency-exchange"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.2", features = [ "derive" ] }
|
||||||
|
reqwest = "0.11.25"
|
||||||
|
rusqlite = { version = "0.31.0", features = ["bundled", "array"] }
|
||||||
|
rusty-money = "0.4.1"
|
||||||
|
serde = { version ="1.0.197", features = ["derive"] }
|
||||||
|
serde_json = "1.0.114"
|
||||||
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
|
rust_decimal = "1.34"
|
||||||
|
rust_decimal_macros = "1.34"
|
|
@ -0,0 +1 @@
|
||||||
|
brace_style="AlwaysNextLine"
|
|
@ -0,0 +1,240 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::{metadata, remove_file},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rusqlite::{Connection, Result};
|
||||||
|
|
||||||
|
use crate::config::{get_cache_path, println_and_exit};
|
||||||
|
|
||||||
|
const CANNOT_CLOSE_MSG: &str = "Couldn't close sqlite connection";
|
||||||
|
|
||||||
|
pub fn check_code(code: &String) -> Result<bool>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let exists: bool = conn.query_row(
|
||||||
|
"SELECT EXISTS(SELECT code FROM currencies WHERE currencies.code = UPPER($1))",
|
||||||
|
[code],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_currencies() -> Result<Vec<[String;2]>>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let mut stmt = conn.prepare("SELECT code, text FROM currencies ORDER BY code")?;
|
||||||
|
let ret = stmt
|
||||||
|
.query_map([], |row| {let v:Result<[String;2]> = Ok([row.get(0)?,row.get(1)?]); v})
|
||||||
|
.expect("Error while listing currencies");
|
||||||
|
|
||||||
|
let mut result: Vec<[String;2]> = Vec::new();
|
||||||
|
for code in ret {
|
||||||
|
let i = code.unwrap();
|
||||||
|
let z = [i[0].clone(),i[1].clone()];
|
||||||
|
result.push(z);
|
||||||
|
}
|
||||||
|
stmt.finalize()?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn list_rates(code_from: &String ) -> Result<Vec<[String;2]>>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let mut stmt = conn.prepare("SELECT code_to, rate FROM exchange_rates WHERE code_from = $1 ORDER BY code_to")?;
|
||||||
|
let ret = stmt
|
||||||
|
.query_map([code_from], |row| {let v:Result<[String;2]> = Ok([row.get(0)?,row.get(1)?]); v})
|
||||||
|
.expect("Error while listing rates");
|
||||||
|
|
||||||
|
let mut result: Vec<[String;2]> = Vec::new();
|
||||||
|
for code in ret {
|
||||||
|
let i = code.unwrap();
|
||||||
|
let z = [i[0].clone(),i[1].clone()];
|
||||||
|
result.push(z);
|
||||||
|
}
|
||||||
|
stmt.finalize()?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_exchange(code_from: &String, code_to: &String) -> Result<bool>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let exists: bool = conn.query_row(
|
||||||
|
"SELECT EXISTS(SELECT code_from, code_to
|
||||||
|
FROM exchange_rates
|
||||||
|
WHERE exchange_rates.code_from = UPPER($1) AND exchange_rates.code_to = UPPER($2))",
|
||||||
|
[code_from, code_to],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rate(code_from: &String, code_to: &String) -> Result<String>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let rate: String = conn.query_row(
|
||||||
|
"SELECT rate
|
||||||
|
FROM exchange_rates
|
||||||
|
WHERE exchange_rates.code_from = UPPER($1) AND exchange_rates.code_to = UPPER($2)",
|
||||||
|
[code_from, code_to],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(rate)
|
||||||
|
}
|
||||||
|
pub fn get_next_update(code: &String) -> Result<u64>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let next_update: u64 = conn.query_row(
|
||||||
|
"SELECT next_update FROM currencies WHERE currencies.code = UPPER($1)",
|
||||||
|
[code],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(next_update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_rates(
|
||||||
|
next_update: u64,
|
||||||
|
code_from: &String,
|
||||||
|
rates: &HashMap<String, serde_json::Value>,
|
||||||
|
) -> Result<()>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
|
||||||
|
for (code_to, rate) in rates {
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT OR REPLACE INTO exchange_rates(code_from,code_to,rate)
|
||||||
|
VALUES(UPPER($1),UPPER($2),$3)
|
||||||
|
",
|
||||||
|
[code_from, code_to, &rate.to_string()],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
UPDATE currencies
|
||||||
|
SET next_update = $1
|
||||||
|
WHERE currencies.code = UPPER($2)
|
||||||
|
",
|
||||||
|
[&next_update.to_string(), code_from],
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_code(code: [String; 2]) -> Result<()>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT OR IGNORE INTO currencies(code,text,next_update)
|
||||||
|
VALUES(UPPER($1),$2,0)
|
||||||
|
",
|
||||||
|
[code.get(0), code.get(1)],
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn get_api_key() -> Result<String>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
let api_key: String = conn.query_row(
|
||||||
|
"SELECT value FROM config WHERE config.name = 'API_KEY'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(api_key)
|
||||||
|
}
|
||||||
|
pub fn set_api_key(key: String) -> Result<()>
|
||||||
|
{
|
||||||
|
let conn = Connection::open(get_cache_path())?;
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
UPDATE config
|
||||||
|
SET value = $1
|
||||||
|
WHERE config.name = 'API_KEY'
|
||||||
|
",
|
||||||
|
[key],
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_cache() -> Result<()>
|
||||||
|
{
|
||||||
|
let path = &get_cache_path();
|
||||||
|
if path.is_dir() {
|
||||||
|
println_and_exit("Specified path cache path is dir, not file")
|
||||||
|
}
|
||||||
|
if path.exists() {
|
||||||
|
match remove_file(path) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(_e) => match metadata(path) {
|
||||||
|
Ok(md) => {
|
||||||
|
if md.permissions().readonly() {
|
||||||
|
println_and_exit("Can't modify file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_e) => println_and_exit("Unknown error while trying to remove old database"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let conn = Connection::open(path)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
CREATE TABLE config (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
CREATE TABLE currencies (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
next_update TIME NOT NULL
|
||||||
|
)",
|
||||||
|
(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
CREATE TABLE exchange_rates (
|
||||||
|
code_from TEXT NOT NULL,
|
||||||
|
code_to TEXT NOT NULL,
|
||||||
|
rate TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (code_from, code_to)
|
||||||
|
)",
|
||||||
|
(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"
|
||||||
|
INSERT INTO config (name, value) VALUES (
|
||||||
|
'API_KEY',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
",
|
||||||
|
(),
|
||||||
|
)?;
|
||||||
|
conn.close().expect(CANNOT_CLOSE_MSG);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use std::{
|
||||||
|
env::{temp_dir, var_os},
|
||||||
|
path::PathBuf,
|
||||||
|
process::exit,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CACHE_LOCATION_ENV_NAME: &str = "CURRENCY_CACHE";
|
||||||
|
pub const REST_ENDPOINT: &str = "https://v6.exchangerate-api.com/v6/";
|
||||||
|
pub const REST_ENDPOINT_ENV_NAME: &str = "CURRENCY_ENDPOINT";
|
||||||
|
|
||||||
|
pub fn get_endpoint() -> String
|
||||||
|
{
|
||||||
|
let ret: String;
|
||||||
|
match var_os(REST_ENDPOINT_ENV_NAME) {
|
||||||
|
Some(val) => ret = val.to_str().unwrap().to_string(),
|
||||||
|
None => ret = REST_ENDPOINT.to_string(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
pub fn get_cache_path() -> PathBuf
|
||||||
|
{
|
||||||
|
let mut path: PathBuf = PathBuf::new();
|
||||||
|
match var_os(CACHE_LOCATION_ENV_NAME) {
|
||||||
|
Some(val) => path.push(val),
|
||||||
|
None => {
|
||||||
|
path.push(temp_dir());
|
||||||
|
path.push("currencyCache.db");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
pub fn get_current_time() -> u64
|
||||||
|
{
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
pub fn println_and_exit(msg: &str)
|
||||||
|
{
|
||||||
|
println!("{}", msg);
|
||||||
|
exit(1)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::*;
|
||||||
|
use rust_decimal::prelude::*;
|
||||||
|
use rusty_money::{iso::find, ExchangeRate, Money};
|
||||||
|
pub async fn update_rate(code: &String)
|
||||||
|
{
|
||||||
|
if cache::get_next_update(code).expect("Error getting next update time from cache")
|
||||||
|
<= config::get_current_time()
|
||||||
|
{
|
||||||
|
let status = requests::get_rates(code)
|
||||||
|
.await
|
||||||
|
.expect("Error while fetching rates");
|
||||||
|
if status == requests::Status::INVALID {
|
||||||
|
config::println_and_exit("Invalid api key when getting rates")
|
||||||
|
} else if status == requests::Status::LIMIT {
|
||||||
|
config::println_and_exit("Exceeded API limit when getting rates")
|
||||||
|
} else if status == requests::Status::ERROR {
|
||||||
|
config::println_and_exit("Unknown error when getting rates")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn get_rate(code_from: &String, code_to: &String) -> String
|
||||||
|
{
|
||||||
|
if !cache::check_code(code_from).expect("Error on getting code status") {
|
||||||
|
config::println_and_exit(
|
||||||
|
format!("Code {} doesn't exists, use correct code!", code_from).as_str(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if !cache::check_code(code_to).expect("Error on getting code status") {
|
||||||
|
config::println_and_exit(
|
||||||
|
format!("Code {} doesn't exists, use correct code!", code_to).as_str(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!cache::check_exchange(code_from, code_to).expect("Error on getting exchange status"))
|
||||||
|
|| (cache::get_next_update(code_from).expect("Error getting next update time from cache")
|
||||||
|
<= config::get_current_time())
|
||||||
|
{
|
||||||
|
update_rate(code_from).await;
|
||||||
|
}
|
||||||
|
cache::get_rate(code_from, code_to).expect("Error when getting cached rate")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn convert_value(code_from: &String, code_to: &String, value: &String)
|
||||||
|
{
|
||||||
|
if value.parse::<f64>().is_err() {
|
||||||
|
config::println_and_exit(format!("{} is not a number!", value).as_str())
|
||||||
|
}
|
||||||
|
let text_rate = get_rate(code_from, code_to).await;
|
||||||
|
let from_currency = find(code_from);
|
||||||
|
if from_currency.is_none() {
|
||||||
|
config::println_and_exit(format!("{} not found in ISO formats", code_from).as_str())
|
||||||
|
}
|
||||||
|
let to_currency = find(code_to);
|
||||||
|
if to_currency.is_none() {
|
||||||
|
config::println_and_exit(format!("{} not found in ISO formats", code_to).as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
let rate = Decimal::from_str(&text_rate).unwrap();
|
||||||
|
let dec_amount = Decimal::from_str(&value).unwrap();
|
||||||
|
let from_money = Money::from_decimal(dec_amount, from_currency.unwrap());
|
||||||
|
println!("Input: {}", from_money.to_string());
|
||||||
|
if code_from != code_to {
|
||||||
|
let ex = ExchangeRate::new(from_currency.unwrap(), to_currency.unwrap(), rate).unwrap();
|
||||||
|
let result = ex.convert(from_money).expect("Error while conversion");
|
||||||
|
println!("Equals: {}", result.to_string())
|
||||||
|
} else {
|
||||||
|
println!("{}", from_money.to_string())
|
||||||
|
}
|
||||||
|
println!("Exchange rate: {}", text_rate);
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cache::{create_cache, set_api_key},
|
||||||
|
requests::get_currencies,
|
||||||
|
};
|
||||||
|
use cache::check_code;
|
||||||
|
use clap::Parser;
|
||||||
|
use exchange::convert_value;
|
||||||
|
mod cache;
|
||||||
|
mod config;
|
||||||
|
mod exchange;
|
||||||
|
mod requests;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(about, long_about = None, arg_required_else_help = true)]
|
||||||
|
struct Cli
|
||||||
|
{
|
||||||
|
/// Currency code to exchange from
|
||||||
|
currency_from: Option<String>,
|
||||||
|
/// Currency code to exchange to
|
||||||
|
currency_to: Option<String>,
|
||||||
|
/// Currency amount to exchange
|
||||||
|
value: Option<String>,
|
||||||
|
|
||||||
|
/// Set api key
|
||||||
|
#[arg(short = 'k', long = "set-api-key")]
|
||||||
|
api_key: Option<String>,
|
||||||
|
/// Recrate cache
|
||||||
|
#[arg(short = 'r', long = "recreate-cache")]
|
||||||
|
recreate_cache: bool,
|
||||||
|
/// Interactive mode
|
||||||
|
#[arg(short, long)]
|
||||||
|
interactive: bool,
|
||||||
|
|
||||||
|
/// List currencies
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: bool,
|
||||||
|
|
||||||
|
/// List currencies
|
||||||
|
#[arg(short = 'L', long = "list-rates")]
|
||||||
|
list_rates: Option<String>,
|
||||||
|
}
|
||||||
|
async fn setup_key(key: String) -> Result<bool, Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
|
set_api_key(key)?;
|
||||||
|
let status = get_currencies().await?;
|
||||||
|
if status == requests::Status::INVALID {
|
||||||
|
set_api_key("".to_string())?;
|
||||||
|
println!("Api Key is invalid");
|
||||||
|
return Ok(false);
|
||||||
|
} else if status == requests::Status::LIMIT {
|
||||||
|
set_api_key("".to_string())?;
|
||||||
|
println!("Can't set up API key due to exceeded API limit");
|
||||||
|
return Ok(false);
|
||||||
|
} else if status == requests::Status::ERROR {
|
||||||
|
set_api_key("".to_string())?;
|
||||||
|
println!("Can't set up API key due to unknown error");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
let all_args =
|
||||||
|
args.currency_from.is_some() && args.currency_to.is_some() && args.value.is_some();
|
||||||
|
let wrong_args =
|
||||||
|
args.currency_from.is_some() && (args.currency_to.is_none() || args.value.is_none());
|
||||||
|
|
||||||
|
if args.interactive && (all_args || wrong_args) {
|
||||||
|
config::println_and_exit("Do not provide codes and value with --interactive")
|
||||||
|
}
|
||||||
|
if args.recreate_cache || !config::get_cache_path().exists() {
|
||||||
|
create_cache()?;
|
||||||
|
}
|
||||||
|
match args.api_key {
|
||||||
|
None => {}
|
||||||
|
Some(key) => {
|
||||||
|
let res = setup_key(key)
|
||||||
|
.await
|
||||||
|
.expect("Unknown error while setting up key");
|
||||||
|
if !res {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !args.interactive {
|
||||||
|
if !(cache::get_api_key()
|
||||||
|
.expect("Error while getting api key")
|
||||||
|
.len()
|
||||||
|
> 0)
|
||||||
|
{
|
||||||
|
config::println_and_exit("API Key is not set up!");
|
||||||
|
}
|
||||||
|
if args.list {
|
||||||
|
let currencies = cache::list_currencies()?;
|
||||||
|
for currency in currencies {
|
||||||
|
println!("{} - {}", currency[0], currency[1]);
|
||||||
|
}
|
||||||
|
} else if args.list_rates.is_some() {
|
||||||
|
let code = args.list_rates.unwrap().clone();
|
||||||
|
let check = check_code(&code)?;
|
||||||
|
if !check {
|
||||||
|
config::println_and_exit(format!("Code {} not found", code).as_str());
|
||||||
|
}
|
||||||
|
exchange::update_rate(&code).await;
|
||||||
|
let rates = cache::list_rates(&code)?;
|
||||||
|
for rate in rates {
|
||||||
|
println!("{} to {} rate: {}", code ,rate[0], rate[1]);
|
||||||
|
}
|
||||||
|
} else if wrong_args {
|
||||||
|
config::println_and_exit(
|
||||||
|
"Not all args specified, provide 'currency from', 'currency to' and 'amount'",
|
||||||
|
);
|
||||||
|
} else if all_args {
|
||||||
|
convert_value(
|
||||||
|
&args.currency_from.unwrap().to_uppercase(),
|
||||||
|
&args.currency_to.unwrap().to_uppercase(),
|
||||||
|
&args.value.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
interactive().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn interactive() -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
|
let mut key_setup = cache::get_api_key()
|
||||||
|
.expect("Error while getting api key")
|
||||||
|
.len()
|
||||||
|
> 0;
|
||||||
|
while !key_setup {
|
||||||
|
let mut key_string = String::new();
|
||||||
|
print!("Please enter API Key: ");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut key_string)
|
||||||
|
.expect("Did not enter a correct string");
|
||||||
|
setup_key(key_string.trim().to_string())
|
||||||
|
.await
|
||||||
|
.expect("Unknown error while setting up key");
|
||||||
|
key_setup = cache::get_api_key()
|
||||||
|
.expect("Error while getting api key")
|
||||||
|
.len()
|
||||||
|
> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut code_from: String = String::new();
|
||||||
|
let mut code_to: String = String::new();
|
||||||
|
let mut amount: String = String::new();
|
||||||
|
|
||||||
|
let mut code_from_check = false;
|
||||||
|
let mut code_to_check = false;
|
||||||
|
let mut amount_check = false;
|
||||||
|
|
||||||
|
while !code_from_check {
|
||||||
|
code_from = String::new();
|
||||||
|
print!("Please enter code of input currency: ");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut code_from)
|
||||||
|
.expect("Did not enter a correct string");
|
||||||
|
code_from = code_from.trim().to_uppercase().to_string();
|
||||||
|
code_from_check = cache::check_code(&code_from)?;
|
||||||
|
if !code_from_check {
|
||||||
|
println!("Code {} is unknown", code_from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while !code_to_check {
|
||||||
|
code_to = String::new();
|
||||||
|
print!("Please enter code of output currency: ");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut code_to)
|
||||||
|
.expect("Did not enter a correct string");
|
||||||
|
code_to = code_to.trim().to_uppercase().to_string();
|
||||||
|
code_to_check = cache::check_code(&code_to)?;
|
||||||
|
if !code_to_check {
|
||||||
|
println!("Code {} is unknown", code_to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while !amount_check {
|
||||||
|
amount = String::new();
|
||||||
|
print!("Please enter amount of input currency: ");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut amount)
|
||||||
|
.expect("Did not enter a correct string");
|
||||||
|
amount = amount.trim().to_string();
|
||||||
|
if amount.parse::<f64>().is_err() {
|
||||||
|
println!("{} is not a number!", amount)
|
||||||
|
} else {
|
||||||
|
amount_check = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
convert_value(&code_from, &code_to, &amount).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::cache::{self, get_api_key};
|
||||||
|
use crate::config::get_endpoint;
|
||||||
|
use serde::Deserialize;
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum Status
|
||||||
|
{
|
||||||
|
OK,
|
||||||
|
INVALID,
|
||||||
|
LIMIT,
|
||||||
|
ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CurrencyCodes
|
||||||
|
{
|
||||||
|
supported_codes: Vec<[String; 2]>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ConversionRates
|
||||||
|
{
|
||||||
|
base_code: String,
|
||||||
|
time_next_update_unix: u64,
|
||||||
|
|
||||||
|
conversion_rates: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Err
|
||||||
|
{
|
||||||
|
#[serde(rename = "error-type")]
|
||||||
|
error_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_rates(code: &String) -> Result<Status, reqwest::Error>
|
||||||
|
{
|
||||||
|
let response = reqwest::get(format!(
|
||||||
|
"{}{}{}{}",
|
||||||
|
get_endpoint(),
|
||||||
|
get_api_key().expect("Error when getting api key from cache"),
|
||||||
|
"/latest/",
|
||||||
|
code.to_uppercase()
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
if response.status().is_success() {
|
||||||
|
let response: ConversionRates =
|
||||||
|
serde_json::from_str(&response.text().await?).expect("Error when deserializng");
|
||||||
|
cache::add_rates(
|
||||||
|
response.time_next_update_unix,
|
||||||
|
&response.base_code,
|
||||||
|
&response.conversion_rates,
|
||||||
|
)
|
||||||
|
.expect("Error while caching response");
|
||||||
|
return Ok(Status::OK);
|
||||||
|
} else {
|
||||||
|
let err: Err =
|
||||||
|
serde_json::from_str(&response.text().await?).expect("Error when deserializng");
|
||||||
|
if err.error_type == "invalid-key" {
|
||||||
|
return Ok(Status::INVALID);
|
||||||
|
} else if err.error_type == "quota-reached" {
|
||||||
|
return Ok(Status::LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Status::ERROR)
|
||||||
|
}
|
||||||
|
pub async fn get_currencies() -> Result<Status, reqwest::Error>
|
||||||
|
{
|
||||||
|
let response = reqwest::get(format!(
|
||||||
|
"{}{}{}",
|
||||||
|
get_endpoint(),
|
||||||
|
get_api_key().expect("Error when getting api key from cache"),
|
||||||
|
"/codes"
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
if response.status().is_success() {
|
||||||
|
let codes: CurrencyCodes =
|
||||||
|
serde_json::from_str(&response.text().await?).expect("Error when deserializng");
|
||||||
|
for code in codes.supported_codes {
|
||||||
|
cache::add_code(code).expect("Error when adding code to cache");
|
||||||
|
}
|
||||||
|
return Ok(Status::OK);
|
||||||
|
} else {
|
||||||
|
let err: Err =
|
||||||
|
serde_json::from_str(&response.text().await?).expect("Error when deserializng");
|
||||||
|
if err.error_type == "invalid-key" {
|
||||||
|
return Ok(Status::INVALID);
|
||||||
|
} else if err.error_type == "quota-reached" {
|
||||||
|
return Ok(Status::LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Status::ERROR)
|
||||||
|
}
|
Reference in New Issue