From f109a65c3faf3a713bb1e723d0e650ff029f947c Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Sat, 29 Jun 2024 16:22:25 -0400 Subject: [PATCH] feat: basic caching support (#25) --- Cargo.lock | 21 +++++++++++ Cargo.toml | 1 + src/cache.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++ src/cli.rs | 4 +++ src/lib.rs | 7 ++++ src/main.rs | 18 +++++++--- src/ports.rs | 22 ++++++++---- src/userstyles.rs | 49 +++++++++++++------------ 8 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 src/cache.rs diff --git a/Cargo.lock b/Cargo.lock index bd60fe0..3b988fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "convert_case", "css-colors", "csscolorparser", + "etcetera", "fancy-regex", "flate2", "graphql_client", @@ -419,6 +420,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -672,6 +684,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" diff --git a/Cargo.toml b/Cargo.toml index ed9b591..3b7a638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ color-eyre = "0.6.3" convert_case = "0.6.0" css-colors = "1.0.1" csscolorparser = { version = "0.6.2", features = ["rgb"] } +etcetera = "0.8.0" fancy-regex = "0.13.0" flate2 = "1.0.30" graphql_client = { version = "0.14.0", features = ["reqwest-blocking"] } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..8473049 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,91 @@ +use color_eyre::eyre::Result; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs, + io::{self, Write}, + path::PathBuf, + time::SystemTime, +}; + +static ONE_DAY_IN_SECONDS: u64 = 24 * 60 * 60; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Entry { + timestamp: SystemTime, + data: String, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Cache { + path: PathBuf, + entries: HashMap, + refresh: bool, +} + +impl Cache { + pub fn new(path: PathBuf, refresh: bool) -> Self { + let entries = match fs::read_to_string(&path) { + Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(), + Err(_) => { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + HashMap::new() + } + }; + + Cache { + path, + entries, + refresh, + } + } + + pub fn get(&self, key: &str) -> Option<&String> { + if self.refresh { + return None; + } + match self.entries.get(key) { + Some(entry) => { + let diff = SystemTime::now() + .duration_since(entry.timestamp) + .unwrap() + .as_secs(); + if diff < ONE_DAY_IN_SECONDS { + Some(&entry.data) + } else { + None + } + } + None => None, + } + } + + pub fn get_or(&mut self, key: &str, fetch: F) -> Result + where + F: FnOnce() -> Result, + { + if let Some(data) = self.get(key) { + return Ok(data.clone()); + } + let value = fetch()?; + self.save(key, value.clone())?; + Ok(value) + } + + pub fn save(&mut self, key: &str, value: String) -> Result { + self.entries.insert( + key.to_string(), + Entry { + timestamp: SystemTime::now(), + data: value.clone(), + }, + ); + self.save_to_file()?; + Ok(value) + } + + fn save_to_file(&self) -> io::Result<()> { + let mut file = fs::File::create(&self.path)?; + file.write_all(serde_json::to_string(&self.entries)?.as_bytes()) + } +} diff --git a/src/cli.rs b/src/cli.rs index 0094205..3c88b4e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,6 +11,10 @@ use crate::models::shared::CATEGORIES; pub struct Cli { #[command(subcommand)] pub command: Commands, + + /// Hard refresh cached data + #[arg(short, long, global = true)] + pub refresh: bool, } #[derive(Subcommand)] diff --git a/src/lib.rs b/src/lib.rs index 0667124..baec940 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use models::{ userstyles::Userstyle, }; +pub mod cache; pub mod cli; pub mod github; pub mod models; @@ -132,3 +133,9 @@ pub fn get_userstyle_key(entry: (String, Userstyle), key: UserstyleKey) -> Value ), } } + +fn fetch_text(url: &str) -> Result { + let response = reqwest::blocking::get(url)?; + let text = response.text()?; + Ok(text) +} diff --git a/src/main.rs b/src/main.rs index 8fcd94d..44de6f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use catppuccin_purr::{ + cache::Cache, cli::{Cli, Commands, Userstyles}, ports, userstyles, whiskerify, }; use clap::Parser; use color_eyre::eyre::Result; +use etcetera::{choose_base_strategy, BaseStrategy}; fn main() -> Result<()> { color_eyre::install()?; @@ -11,7 +13,15 @@ fn main() -> Result<()> { .filter_level(log::LevelFilter::Warn) .init(); - let args = Cli::parse(); + let args: Cli = Cli::parse(); + + let cache = Cache::new( + choose_base_strategy() + .unwrap() + .cache_dir() + .join("purr/store.json"), + args.refresh, + ); match args.command { Commands::Query { @@ -19,7 +29,7 @@ fn main() -> Result<()> { r#for, count, get, - } => ports::query(command, r#for, count, get)?, + } => ports::query(cache, command, r#for, count, get)?, Commands::Init { name, url } => ports::init(name, url)?, Commands::Userstyles { command } => match command { Userstyles::Query { @@ -27,14 +37,14 @@ fn main() -> Result<()> { r#for, count, get, - } => userstyles::query(command, r#for, count, get)?, + } => userstyles::query(cache, command, r#for, count, get)?, Userstyles::Init { name, categories, icon, color, url, - } => userstyles::init(name, categories, icon, color, url)?, + } => userstyles::init(cache, name, categories, icon, color, url)?, }, Commands::Whiskerify { input, output } => whiskerify::handle(input, output)?, } diff --git a/src/ports.rs b/src/ports.rs index a263ed7..a8e0281 100644 --- a/src/ports.rs +++ b/src/ports.rs @@ -7,20 +7,28 @@ use inquire::Text; use serde_json::Value; use url::Url; +use crate::cache::Cache; use crate::cli::{Key, Query, WhiskersCustomProperty}; use crate::github::{ self, fetch_all_repositories, fetch_whiskers_custom_property, RepositoryResponse, }; use crate::models::ports::Root; use crate::models::shared::StringOrStrings; -use crate::{display_json_or_count, get_key, is_booleanish_match, matches_current_maintainer}; +use crate::{ + display_json_or_count, fetch_text, get_key, is_booleanish_match, matches_current_maintainer, +}; -pub fn query(command: Option, r#for: Option, count: bool, get: Key) -> Result<()> { - let raw: String = reqwest::blocking::get( - "https://github.com/catppuccin/catppuccin/raw/main/resources/ports.yml", - )? - .text()?; - let data: Root = serde_yaml::from_str(&raw).unwrap(); +pub fn query( + mut cache: Cache, + command: Option, + r#for: Option, + count: bool, + get: Key, +) -> Result<()> { + let ports = cache.get_or("ports-yml", || { + fetch_text("https://github.com/catppuccin/catppuccin/raw/main/resources/ports.yml") + })?; + let data: Root = serde_yaml::from_str(&ports).unwrap(); match command { Some(Query::Maintained { by, options }) => { diff --git a/src/userstyles.rs b/src/userstyles.rs index 5cd5c72..8a005fd 100644 --- a/src/userstyles.rs +++ b/src/userstyles.rs @@ -7,24 +7,26 @@ use inquire::validator::Validation; use inquire::{MultiSelect, Select, Text}; use url::Url; +use crate::cache::Cache; use crate::cli::{UserstyleKey, UserstylesQuery}; use crate::models::shared::{StringOrStrings, CATEGORIES}; use crate::models::userstyles::{Readme, Root, Userstyle, UserstylesRoot}; use crate::{ - display_json_or_count, get_userstyle_key, is_booleanish_match, matches_current_maintainer, + display_json_or_count, fetch_text, get_userstyle_key, is_booleanish_match, + matches_current_maintainer, }; pub fn query( + mut cache: Cache, command: Option, r#for: Option, count: bool, get: UserstyleKey, ) -> Result<()> { - let raw: String = reqwest::blocking::get( - "https://github.com/catppuccin/userstyles/raw/main/scripts/userstyles.yml", - )? - .text()?; - let data: Root = serde_yaml::from_str(&raw).unwrap(); + let userstyles = cache.get_or("userstyles-yml", || { + fetch_text("https://github.com/catppuccin/userstyles/raw/main/scripts/userstyles.yml") + })?; + let data: Root = serde_yaml::from_str(&userstyles).unwrap(); match command { Some(UserstylesQuery::Maintained { by, options }) => { @@ -147,6 +149,7 @@ pub fn query( } pub fn init( + mut cache: Cache, name: Option, categories: Option>, icon: Option, @@ -215,22 +218,24 @@ pub fn init( } fs::create_dir(&target)?; - let mut template: String = reqwest::blocking::get( - "https://github.com/catppuccin/userstyles/raw/main/template/catppuccin.user.css", - )? - .text()? - .replace(" Catppuccin", &format!("{} Catppuccin", &name)) - .replace( - "Soothing pastel theme for ", - &format!("Soothing pastel theme for {}", &name), - ) - .replace("", &name_kebab) - .replace( - "", - Url::parse(&url)? - .host_str() - .expect("App link should be a valid URL"), - ); + let mut template = cache + .get_or("userstyles-template", || { + fetch_text( + "https://github.com/catppuccin/userstyles/raw/main/template/catppuccin.user.css", + ) + })? + .replace(" Catppuccin", &format!("{} Catppuccin", &name)) + .replace( + "Soothing pastel theme for ", + &format!("Soothing pastel theme for {}", &name), + ) + .replace("", &name_kebab) + .replace( + "", + Url::parse(&url)? + .host_str() + .expect("App link should be a valid URL"), + ); let comment_re = fancy_regex::Regex::new(r"\/\*(?:(?!\*\/|==UserStyle==|prettier-ignore)[\s\S])*?\*\/")?;