Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache-related CLI options #8

Merged
merged 4 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,33 @@ macro_rules! parse_response {

pub struct Client {
cfg: Config,
http: reqwest::Client,
http: Option<reqwest::Client>,
cache: Option<BitlinkCache>,
}

// TODO: handle timeouts, cancellation, API limits (see `GET /v4/user/platform_limits`), etc.
impl Client {
pub async fn new(cfg: Config) -> Self {
let http = reqwest::Client::new();
let http = if cfg.offline {
None
} else {
Some(reqwest::Client::new())
};

let cache = BitlinkCache::new(VERSION, cfg.cache_dir.as_ref()).await;

Self { cfg, http, cache }
}

pub async fn fetch_user(&self) -> Result<User> {
let Some(ref http) = self.http else {
return Err(Error::Offline("user"));
};

let endpoint = api_url("user");

//println!("fetching user info");
let resp = self
.http
let resp = http
.get(endpoint)
.bearer_auth(self.cfg.api_token())
.send()
Expand Down Expand Up @@ -134,9 +143,6 @@ impl Client {

let domain = self.cfg.domain.as_deref().map(Cow::Borrowed);

// TODO: cache links in a local sqlite DB
// - use e.g. `$XDG_CACHE_HOME/bitly/links`
// - add `--offline` mode (possibly conflicts with `--no-cache`)
let payload = Shorten {
long_url,
domain,
Expand All @@ -150,11 +156,14 @@ impl Client {
}
}

let Some(ref http) = self.http else {
return Err(Error::Offline("shorten"));
};

let endpoint = api_url("shorten");

//println!("sending shorten request: {payload:#?}");
let resp = self
.http
let resp = http
.post(endpoint)
.bearer_auth(self.cfg.api_token())
.json(&payload)
Expand Down
58 changes: 47 additions & 11 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::borrow::Cow;
use std::path::{Path, PathBuf};

use clap::{Args, Parser, Subcommand};
use clap::builder::ArgPredicate;
use clap::{Args, Parser, Subcommand, ValueHint};
use url::Url;

use crate::config::{ConfigError, APP};
use crate::config::{ConfigError, Options, APP};

#[derive(Debug, Parser)]
#[command(name = APP)]
Expand All @@ -15,21 +16,39 @@ pub struct Cli {
command: Option<Command>,

/// Alternative path to the config file (TOML)
#[arg(short, long, env = "BITCLI_CONFIG_FILE")]
#[arg(short, long, env = "BITCLI_CONFIG_FILE", value_hint = ValueHint::FilePath)]
config_file: Option<PathBuf>,

/// Alternative path to the cache directory
///
/// If set to an empty path, then caching will be disabled.
#[arg(long, env = "BITCLI_CACHE_DIR")]
#[arg(long, env = "BITCLI_CACHE_DIR", value_hint = ValueHint::DirPath)]
cache_dir: Option<PathBuf>,

// TODO: --no-cache | -nc => explicitly disable caching
// - global vs ShortenArgs flag
// - possibly implement by adding a `cache_dir()` getter that combines all the cache* fields
// - conflicts with vs overrides `cache_dir` option
//#[arg(short, long, default_value = "false", env = "BITCLI_NO_CACHE")]
//no_cache: bool,
/// Explicitly disable local cache for this command invocation
///
/// Equivalent to passing an empty `--cache-dir` path. Takes priority over `--cache-dir`.
#[arg(
long,
default_value = "false",
overrides_with = "cache_dir",
env = "BITCLI_NO_CACHE"
)]
no_cache: bool,

/// Enabling the offline mode will prevent any API requests
///
/// Under this mode, any command will only rely on the local cache, therefore this flag cannot
/// be combined with `--no-cache`. Furthermore, it's automatically disabled when `--cache-dir`
/// is set to an empty path (which disables caching).
#[arg(
long,
default_value = "false",
default_value_if("cache_dir", ArgPredicate::Equals("".into()), "false"),
conflicts_with = "no_cache",
env = "BITCLI_OFFLINE"
)]
offline: bool,

// emulate default (sub)command
#[clap(flatten)]
Expand All @@ -53,6 +72,23 @@ impl Cli {
}
}

impl From<&Cli> for Options {
fn from(cli: &Cli) -> Self {
let mut ops = Self::default();

if cli.no_cache {
// NOTE: empty path for the `cache_dir` disables the cache
ops.cache_dir = Some(PathBuf::new());
} else {
ops.cache_dir.clone_from(&cli.cache_dir);
}

ops.offline = Some(cli.offline);

ops
}
}

#[derive(Debug, Subcommand)]
pub enum Command {
#[command(about = "Shorten URL and print the result to the output (default)")]
Expand All @@ -66,7 +102,7 @@ impl From<Cli> for Command {
}
}

impl From<&Command> for crate::config::Options {
impl From<&Command> for Options {
fn from(cmd: &Command) -> Self {
let mut ops = Self::default();

Expand Down
40 changes: 28 additions & 12 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub struct Config {
///
/// If set to an empty path, then caching will be disabled.
pub cache_dir: Option<PathBuf>,

/// If set to `true` then no API requests will be issued (disabled by default)
///
/// Any command will only rely on the local cache under the _offline_ mode.
#[serde(default = "default::offline")]
pub offline: bool,
}

impl Config {
Expand Down Expand Up @@ -66,22 +72,22 @@ impl Config {

/// Update current configs with _some_ of the given options (only those that are `Some`)
pub fn override_with(&mut self, ops: impl Into<Options>) {
let Options {
domain,
group_guid,
cache_dir,
} = ops.into();

if domain.is_some() {
self.domain = domain;
let ops = ops.into();

if ops.domain.is_some() {
self.domain = ops.domain;
}

if ops.group_guid.is_some() {
self.default_group_guid = ops.group_guid;
}

if group_guid.is_some() {
self.default_group_guid = group_guid;
if ops.cache_dir.is_some() {
self.cache_dir = ops.cache_dir;
}

if cache_dir.is_some() {
self.cache_dir = cache_dir;
if let Some(offline) = ops.offline {
self.offline = offline;
}
}

Expand All @@ -91,6 +97,13 @@ impl Config {
}
}

mod default {
#[inline]
pub(super) fn offline() -> bool {
false
}
}

#[derive(Debug, Default)]
pub struct Options {
/// The domain to create bitlinks under (defaults to `bit.ly` if unspecified)
Expand All @@ -101,6 +114,9 @@ pub struct Options {

/// Alternative path to the cache directory
pub cache_dir: Option<PathBuf>,

/// Controls whether issuing API requests is allowed
pub offline: Option<bool>,
}

#[derive(Debug, Deserialize)]
Expand Down
9 changes: 7 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use serde::Deserialize;

use crate::config::APP;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Cannot determine group GOUID: {0}")]
#[error("{APP}: operation '{0}' could not complete under offline mode")]
Offline(&'static str),

#[error("{APP}: cannot determine group GOUID: {0}")]
UnknownGroupGUID(&'static str),

#[error(transparent)]
Expand All @@ -16,7 +21,7 @@ pub enum Error {

#[derive(Debug, Deserialize, thiserror::Error)]
#[error(
"Bitly request failed with {message} ({}): {} | {:?}",
"{APP}: Bitly request failed with {message} ({}): {} | {:?}",
resource.as_deref().unwrap_or("?"),
description.as_deref().unwrap_or("?"),
errors.as_deref().unwrap_or_default(),
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ macro_rules! crash_if_err {

#[tokio::main(flavor = "current_thread")]
async fn main() {
let args = Cli::parse();
let cli = Cli::parse();

let mut cfg = crash_if_err! { args.config_file().and_then(Config::load) };
let mut cfg = crash_if_err! { cli.config_file().and_then(Config::load) };
cfg.override_with(&cli);

let cmd = args.into();
let cmd = cli.into();
cfg.override_with(&cmd);

let client = Client::new(cfg).await;
Expand Down