-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
331 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
use anyhow::Result; | ||
use reqwest::blocking::get; | ||
use url::Url; | ||
|
||
pub fn get_data(url: Url) -> Result<String> { | ||
get(url)?.text().map_err(|e| e.into()) | ||
} | ||
|
||
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct Statistics { | ||
pub message: String, | ||
pub data: Data, | ||
} | ||
|
||
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct Data { | ||
pub date: String, | ||
pub day: i64, | ||
pub resource: String, | ||
pub stats: Stats, | ||
pub increase: Increase, | ||
} | ||
|
||
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct Stats { | ||
#[serde(rename = "personnel_units")] | ||
pub personnel_units: i64, | ||
pub tanks: i64, | ||
#[serde(rename = "armoured_fighting_vehicles")] | ||
pub armoured_fighting_vehicles: i64, | ||
#[serde(rename = "artillery_systems")] | ||
pub artillery_systems: i64, | ||
pub mlrs: i64, | ||
#[serde(rename = "aa_warfare_systems")] | ||
pub aa_warfare_systems: i64, | ||
pub planes: i64, | ||
pub helicopters: i64, | ||
#[serde(rename = "vehicles_fuel_tanks")] | ||
pub vehicles_fuel_tanks: i64, | ||
#[serde(rename = "warships_cutters")] | ||
pub warships_cutters: i64, | ||
#[serde(rename = "cruise_missiles")] | ||
pub cruise_missiles: i64, | ||
#[serde(rename = "uav_systems")] | ||
pub uav_systems: i64, | ||
#[serde(rename = "special_military_equip")] | ||
pub special_military_equip: i64, | ||
#[serde(rename = "atgm_srbm_systems")] | ||
pub atgm_srbm_systems: i64, | ||
} | ||
|
||
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct Increase { | ||
#[serde(rename = "personnel_units")] | ||
pub personnel_units: i64, | ||
pub tanks: i64, | ||
#[serde(rename = "armoured_fighting_vehicles")] | ||
pub armoured_fighting_vehicles: i64, | ||
#[serde(rename = "artillery_systems")] | ||
pub artillery_systems: i64, | ||
pub mlrs: i64, | ||
#[serde(rename = "aa_warfare_systems")] | ||
pub aa_warfare_systems: i64, | ||
pub planes: i64, | ||
pub helicopters: i64, | ||
#[serde(rename = "vehicles_fuel_tanks")] | ||
pub vehicles_fuel_tanks: i64, | ||
#[serde(rename = "warships_cutters")] | ||
pub warships_cutters: i64, | ||
#[serde(rename = "cruise_missiles")] | ||
pub cruise_missiles: i64, | ||
#[serde(rename = "uav_systems")] | ||
pub uav_systems: i64, | ||
#[serde(rename = "special_military_equip")] | ||
pub special_military_equip: i64, | ||
#[serde(rename = "atgm_srbm_systems")] | ||
pub atgm_srbm_systems: i64, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
use std::fs::File; | ||
use std::io::{ErrorKind, Read, Write}; | ||
use std::path::{PathBuf}; | ||
use anyhow::{anyhow, Result}; | ||
use humantime::Duration; | ||
use std::time::SystemTime; | ||
use log::{debug, info, warn}; | ||
|
||
// Create a new instance of Cache and read the cache from the file | ||
pub fn read(cache_dir: PathBuf, ttl: humantime::Duration) -> Result<Cache> { | ||
let mut cache = Cache::new(cache_dir, ttl); | ||
debug!("Init cache: {:?}", &cache); | ||
info!("Reading cache from fle: {:?}", cache.file.as_path()); | ||
cache.read()?; | ||
debug!("Cache read: {:?}", &cache); | ||
Ok(cache) | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct Cache { | ||
pub file: PathBuf, | ||
pub ttl: Duration, | ||
pub data: Data, | ||
} | ||
|
||
impl Cache { | ||
pub fn new(cache_dir: PathBuf, ttl: Duration) -> Cache { | ||
Cache { | ||
file: cache_dir.join("cache.json"), | ||
data: Data::NotChecked, | ||
ttl, | ||
} | ||
} | ||
|
||
pub fn write(&mut self, data: &str) -> Result<()> { | ||
let mut file = File::create(&self.file)?; | ||
file.write_all(data.as_bytes())?; | ||
Ok(()) | ||
} | ||
|
||
pub fn read(&mut self) -> Result<&Self> { | ||
match File::open(&self.file) { | ||
Ok(mut f) => { | ||
info!("Cache file exists: {:?}", &self.file); | ||
info!("Reading cache file..."); | ||
let mut s = String::new(); | ||
f.read_to_string(&mut s)?; | ||
debug!("Cache file read: {:?}", &s); | ||
|
||
info!("Checking cache file age..."); | ||
let mills_since_modified = Duration::from(SystemTime::now().duration_since(f.metadata()?.modified()?)?); | ||
info!("Cache file age: {}", mills_since_modified); | ||
self.data = if mills_since_modified.as_micros() > self.ttl.as_micros() { | ||
info!("Cache file is outdated"); | ||
Data::Outdated(s) | ||
} else { | ||
info!("Cache file is not outdated"); | ||
Data::Ready(s) | ||
} | ||
} | ||
Err(e) => { | ||
warn!("Error opening cache file: {:?}", e); | ||
if e.kind() == ErrorKind::NotFound { | ||
info!("Cache file does not exist: {:?}", &self.file); | ||
info!("Creating cache file..."); | ||
File::create(&self.file)?; | ||
self.data = Data::None | ||
} else { | ||
return Err(anyhow!("Error opening cache file: {}", e)); | ||
} | ||
} | ||
} | ||
Ok(self) | ||
} | ||
} | ||
|
||
|
||
#[derive(Debug, PartialEq, Clone)] | ||
pub enum Data { | ||
Ready(String), | ||
Outdated(String), | ||
None, | ||
NotChecked, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::io::Write; | ||
use std::str::FromStr; | ||
use tempfile; | ||
// Note this useful idiom: importing names from outer (for mod tests) scope. | ||
use super::*; | ||
|
||
// Test Cache.get_cache() fail | ||
#[test] | ||
fn get_cache_fail() { | ||
let mut cache = Cache::new(PathBuf::from("/nonexist/"), Duration::from_str("1s").unwrap()); | ||
assert!(cache.read().is_err()); | ||
} | ||
|
||
// Test Cache.get_cache() not found | ||
#[test] | ||
fn get_cache_none() { | ||
let dir = tempfile::tempdir().expect("Can't create a temp dir").into_path(); | ||
let mut cache = Cache::new(dir.clone(), Duration::from_str("1s").unwrap()); | ||
assert!(matches!(cache.read(), Ok(_))); | ||
assert!(matches!(cache.data, Data::None)); | ||
// Check file is created | ||
assert!( | ||
!File::open(format!("{}/cache.json", dir.to_str().unwrap())).is_err() | ||
) | ||
} | ||
|
||
// Test Cache.get_cache() outdated | ||
#[test] | ||
fn get_cache_outdated() { | ||
let dir = tempfile::tempdir().expect("Can't create a temp dir").into_path(); | ||
let mut cache = Cache::new(dir.clone(), Duration::from_str("0 ns").unwrap()); | ||
let mut file = File::create(dir.clone().join("cache.json")).unwrap(); | ||
write!(file, "data").expect("Fila should contain info"); | ||
assert!(matches!(cache.read(), Ok(_))); | ||
assert_eq!(cache.data, Data::Outdated("data".to_string())); | ||
// Check file is created | ||
assert!( | ||
!File::open(format!("{}/cache.json", dir.to_str().unwrap())).is_err() | ||
) | ||
} | ||
|
||
// Test Cache.get_cache() ready | ||
#[test] | ||
fn get_cache_ready() { | ||
let dir = tempfile::tempdir().expect("Can't create a temp dir").into_path(); | ||
let mut cache = Cache::new(dir.clone(), Duration::from_str("1 s").unwrap()); | ||
let mut file = File::create(dir.clone().join("cache.json")).unwrap(); | ||
write!(file, "data").expect("Fila should contain info"); | ||
assert!(matches!(cache.read(), Ok(_))); | ||
assert_eq!(cache.data, Data::Ready("data".to_string())); | ||
// Check file is created | ||
assert!( | ||
!File::open(format!("{}/cache.json", dir.to_str().unwrap())).is_err() | ||
) | ||
} | ||
|
||
// Test Cache.write() fail | ||
#[test] | ||
fn write_fail() { | ||
let dir = PathBuf::from_str("/nonexist/").unwrap(); | ||
let mut cache = Cache::new(dir.clone(), Duration::from_str("1s").unwrap()); | ||
assert!(cache.write("data").is_err()); | ||
} | ||
|
||
// Test Cache.write() success | ||
#[test] | ||
fn write_success() { | ||
let dir = tempfile::tempdir().expect("Can't create a temp dir").into_path(); | ||
let mut cache = Cache::new(dir.clone(), Duration::from_str("1s").unwrap()); | ||
assert!(matches!(cache.write("data"), Ok(_))); | ||
assert!( | ||
!File::open(format!("{}/cache.json", dir.to_str().unwrap())).is_err() | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use std::fs::DirBuilder; | ||
use std::path::PathBuf; | ||
use anyhow::{Result, Context}; | ||
use log::debug; | ||
|
||
pub fn config_dir() -> Result<PathBuf> { | ||
let cfg_dir = dirs::config_dir() | ||
.unwrap_or(PathBuf::new()) | ||
.join("shoporusni"); | ||
debug!("Config dir: {:?}", cfg_dir); | ||
// Create a new config dir if it doesn't exist | ||
DirBuilder::new() | ||
.recursive(true) | ||
.create(&cfg_dir) | ||
.context("Creating a new config directory")?; | ||
Ok(cfg_dir) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
use anyhow::{anyhow, Context, Result}; | ||
use log::{debug, info}; | ||
use colored::Colorize; | ||
|
||
pub mod api; | ||
pub mod cache; | ||
pub mod config; | ||
|
||
|
||
pub fn get_data(url: url::Url, mut c: cache::Cache) -> Result<api::Statistics> { | ||
info!("Trying to decide — get data from API or from cache"); | ||
let data = c.data.clone(); | ||
match data { | ||
cache::Data::Ready(ref s) => { | ||
info!("Cache is ready, no need to get API"); | ||
serde_json::from_str(s).context("Error parsing cache data") | ||
}, | ||
cache::Data::Outdated(ref s) => { | ||
info!("Cache is outdated"); | ||
api::get_data(url) | ||
.and_then(|d| serde_json::from_str(&d).map_err(|e| e.into())) | ||
.and_then(|d: api::Statistics| { | ||
info!("Got data from API, writing cache"); | ||
info!("Cache data: {:?}", &d); | ||
c.write(s)?; | ||
info!("Cache written"); | ||
Ok(d) | ||
}) | ||
.or_else(|_| serde_json::from_str(s).map_err(|e| e.into())) | ||
}, | ||
cache::Data::None => { | ||
info!("Cache is empty"); | ||
info!("Getting data from API"); | ||
let data = api::get_data(url)?; | ||
debug!("Got data from API: {:?}", &data); | ||
info!("Got data from API, trying to parse it"); | ||
let result: api::Statistics = serde_json::from_str(&data).context("Error serializing data")?; | ||
debug!("Parsed data: {:?}", &result); | ||
info!("Parsed data, writing cache"); | ||
c.write(&data)?; | ||
info!("Cache written"); | ||
Ok(result) | ||
} | ||
_ => Err(anyhow!("Cache in wired state...")), | ||
} | ||
} | ||
|
||
pub fn print_result(data: &api::Statistics) { | ||
let mil = data.data.stats.personnel_units.to_string().red(); | ||
let mil_inc = data.data.increase.personnel_units.to_string().green(); | ||
print!("{}↑{}", mil, mil_inc) | ||
} |
Oops, something went wrong.