use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CredentialType {
Interactive,
Key,
}
impl std::fmt::Display for CredentialType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Interactive => f.write_str("Interactive"),
Self::Key => f.write_str("API Key"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credentials {
pub name: String,
#[serde(rename = "type")]
pub cred_type: CredentialType,
pub email: String,
pub created_at: DateTime<Utc>,
pub last_used: DateTime<Utc>,
pub token: Option<String>,
}
impl Credentials {
pub fn new(
name: impl Into<String>,
cred_type: CredentialType,
token: impl Into<String>,
email: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
name: name.into(),
cred_type,
email: email.into(),
created_at: now,
last_used: now,
token: Some(token.into()),
}
}
pub fn label(&self) -> String {
match self.cred_type {
CredentialType::Key => format!("{} [{}]", self.name, self.email),
CredentialType::Interactive => self.email.clone(),
}
}
pub fn touch(&mut self) {
self.last_used = Utc::now();
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CredentialsConfig {
pub default: Option<String>,
pub credentials: BTreeMap<String, Credentials>,
}
impl CredentialsConfig {
pub fn add(&mut self, cred: Credentials) {
let name = cred.name.clone();
self.credentials.insert(name.clone(), cred);
if self.default.is_none() {
self.default = Some(name);
}
}
pub fn remove(&mut self, name: &str) -> bool {
if self.credentials.remove(name).is_some() {
if self.default.as_deref() == Some(name) {
self.default = self.credentials.keys().next().cloned();
}
true
} else {
false
}
}
#[allow(dead_code)]
pub fn default_credentials(&self) -> Option<&Credentials> {
self.default
.as_deref()
.and_then(|name| self.credentials.get(name))
}
pub fn set_default(&mut self, name: &str) -> bool {
if self.credentials.contains_key(name) {
self.default = Some(name.to_owned());
true
} else {
false
}
}
pub fn find_by_email_and_type(
&self,
email: &str,
cred_type: CredentialType,
) -> Option<&Credentials> {
self.credentials
.values()
.find(|c| c.email == email && c.cred_type == cred_type)
}
pub fn unique_name(&self, base: &str) -> String {
if !self.credentials.contains_key(base) {
return base.to_owned();
}
for i in 1.. {
let candidate = format!("{base}-{i}");
if !self.credentials.contains_key(&candidate) {
return candidate;
}
}
unreachable!()
}
}