pub mod cache;
pub mod paths;
pub mod store;
pub mod timers;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use figment::Figment;
use figment::providers::{Env, Format, Toml};
use serde::{Deserialize, Serialize};
use crate::render::Format as OutputFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
#[value(rename_all = "kebab-case")]
pub enum OrgKind {
Cloud,
Yandex360,
}
impl OrgKind {
#[must_use]
pub fn header_name(self) -> &'static str {
match self {
Self::Cloud => "x-cloud-org-id",
Self::Yandex360 => "x-org-id",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Account {
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Display {
pub limit: usize,
pub max: usize,
pub description_lines: usize,
pub description_lines_human: Option<usize>,
pub extra_fields: Vec<String>,
pub format: OutputFormat,
pub images: bool,
}
impl Default for Display {
fn default() -> Self {
Self {
limit: 25,
max: 500,
description_lines: 10,
description_lines_human: None,
extra_fields: Vec::new(),
format: OutputFormat::Text,
images: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
pub account: String,
pub org_id: String,
pub org_kind: OrgKind,
#[serde(default)]
pub default_queue: Option<String>,
#[serde(default)]
pub display: Display,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub default_profile: Option<String>,
pub accounts: BTreeMap<String, Account>,
pub profiles: BTreeMap<String, Profile>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ProjectPin {
pub profile: Option<String>,
pub queue: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProfileSource {
Flag,
Env,
ProjectFile(PathBuf),
DefaultProfile,
QueueOwner(String),
Qualified(String),
}
impl std::fmt::Display for ProfileSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Flag => f.write_str("--profile"),
Self::Env => f.write_str("YTCLI_PROFILE"),
Self::ProjectFile(path) => write!(f, "{}", path.display()),
Self::DefaultProfile => f.write_str("config default_profile"),
Self::QueueOwner(queue) => write!(f, "the only profile that sees {queue}"),
Self::Qualified(target) => write!(f, "the key `{target}`"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error(
"no profile selected: pass --profile, set YTCLI_PROFILE, add .tracker.toml, or set default_profile"
)]
NoProfile,
#[error("profile `{0}` is not defined in the config file")]
UnknownProfile(String),
#[error("profile `{profile}` refers to account `{account}`, which is not defined")]
UnknownAccount { profile: String, account: String },
#[error("could not read configuration")]
Read(#[from] figment::Error),
#[error("could not locate the configuration directory")]
Paths(#[from] paths::PathsError),
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub name: String,
pub profile: Profile,
pub source: ProfileSource,
pub queue: Option<String>,
}
impl Config {
pub fn load(config_file: &Path) -> Result<Self, ConfigError> {
Ok(Figment::new()
.merge(Toml::file(config_file))
.merge(Env::prefixed("YTCLI_").split("__"))
.extract()?)
}
pub fn resolve(
&self,
flag: Option<&str>,
env: Option<&str>,
start_dir: &Path,
) -> Result<Resolved, ConfigError> {
let pin = paths::find_project_pin(start_dir);
let (name, source) = match (flag, env, &pin) {
(Some(name), _, _) => (name.to_owned(), ProfileSource::Flag),
(None, Some(name), _) => (name.to_owned(), ProfileSource::Env),
(None, None, Some((path, pinned))) if pinned.profile.is_some() => {
let Some(name) = pinned.profile.clone() else {
return Err(ConfigError::NoProfile);
};
(name, ProfileSource::ProjectFile(path.clone()))
}
_ => {
let name = self.default_profile.clone().ok_or(ConfigError::NoProfile)?;
(name, ProfileSource::DefaultProfile)
}
};
let profile = self
.profiles
.get(&name)
.cloned()
.ok_or_else(|| ConfigError::UnknownProfile(name.clone()))?;
if !self.accounts.contains_key(&profile.account) {
return Err(ConfigError::UnknownAccount {
profile: name,
account: profile.account,
});
}
Ok(Resolved {
name,
queue: pin
.as_ref()
.and_then(|(_, pinned)| pinned.queue.clone())
.or_else(|| profile.default_queue.clone()),
profile,
source,
})
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn images_are_on_by_default_and_can_be_turned_off() {
assert!(Display::default().images);
let display: Display = figment::Figment::new()
.merge(figment::providers::Toml::string("images = false"))
.extract()
.expect("parses");
assert!(!display.images);
assert_eq!(display.limit, Display::default().limit);
}
}