use crate::error;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
env,
path::{Path, PathBuf},
};
use tokio::{fs, io::AsyncWriteExt};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub __config__: Option<ConfigTable>,
#[serde(flatten)]
pub packages: BTreeMap<String, Package>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ConfigTable {
pub oldver: Option<String>,
pub newver: Option<String>,
pub(crate) keyfile: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Package {
source: String, #[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
host: String,
#[cfg(feature = "aur")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
aur: String,
#[cfg(feature = "crates-io")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
cratesio: String,
#[cfg(feature = "gitea")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
gitea: String,
#[cfg(feature = "github")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
github: String,
#[cfg(feature = "gitlab")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
gitlab: String,
#[cfg(feature = "regex")]
#[serde(default)]
url: String,
#[cfg(feature = "regex")]
#[serde(default)]
regex: String,
#[cfg(feature = "shell")]
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
shell: String,
#[serde(default)]
pub(crate) use_max_tag: Option<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "is_empty_string")]
pub prefix: String,
}
impl Package {
pub fn new(
source: String,
target: String,
use_max_tag: bool,
prefix: String,
) -> error::Result<Self> {
let mut package = Package::default();
match source.as_str() {
#[cfg(feature = "aur")]
"aur" => {
package.aur = target;
Ok(())
}
#[cfg(feature = "crates-io")]
"cratesio" => {
package.cratesio = target;
Ok(())
}
#[cfg(feature = "gitea")]
"gitea" => {
package.gitea = target;
Ok(())
}
#[cfg(feature = "github")]
"github" => {
package.github = target;
Ok(())
}
#[cfg(feature = "gitlab")]
"gitlab" => {
package.gitlab = target;
Ok(())
}
#[cfg(feature = "regex")]
"regex" => {
package.url = target;
Ok(())
}
#[cfg(feature = "shell")]
"shell" => {
package.shell = target;
Ok(())
}
_ => Err(error::Error::SourceNotFound(source.clone())),
}?;
package.source = source;
package.use_max_tag = Some(use_max_tag);
package.prefix = prefix;
Ok(package)
}
fn default() -> Self {
Package {
source: String::new(),
host: String::new(),
#[cfg(feature = "aur")]
aur: String::new(),
#[cfg(feature = "crates-io")]
cratesio: String::new(),
#[cfg(feature = "gitea")]
gitea: String::new(),
#[cfg(feature = "github")]
github: String::new(),
#[cfg(feature = "gitlab")]
gitlab: String::new(),
#[cfg(feature = "regex")]
url: String::new(),
#[cfg(feature = "regex")]
regex: String::new(),
#[cfg(feature = "shell")]
shell: String::new(),
use_max_tag: None,
prefix: String::new(),
}
}
pub fn get_api(&self) -> (String, Vec<String>) {
let self_ref = self.to_owned();
let args = match self.source.as_str() {
#[cfg(feature = "aur")]
"aur" => vec![self_ref.aur],
#[cfg(feature = "crates-io")]
"cratesio" => vec![self_ref.cratesio],
#[cfg(feature = "gitea")]
"gitea" => vec![self_ref.gitea, self_ref.host],
#[cfg(feature = "github")]
"github" => vec![self_ref.github],
#[cfg(feature = "gitlab")]
"gitlab" => vec![self_ref.gitlab, self_ref.host],
#[cfg(feature = "regex")]
"regex" => vec![self_ref.url, self_ref.regex],
#[cfg(feature = "shell")]
"shell" => vec![self_ref.shell],
_ => vec![],
};
(self_ref.source, args)
}
}
pub async fn load(custom_path: &Option<String>) -> error::Result<(Config, PathBuf)> {
let config_path = if let Some(path) = custom_path {
let path = Path::new(&path);
if path.exists() && path.is_file() {
path.to_path_buf()
} else {
return Err(error::Error::NoConfigSpecified);
}
} else {
let default_path = Path::new("nvrs.toml");
let config_home = format!(
"{}/nvrs/nvrs.toml",
env::var("XDG_CONFIG_HOME").unwrap_or(expand_tilde("~/.config".to_string())?)
);
let config_home_non_xdg = expand_tilde("~/.config/nvrs.toml".to_string())?;
let config_home_non_xdg = Path::new(&config_home_non_xdg);
let home_path = Path::new(&config_home);
if default_path.exists() && default_path.is_file() {
default_path.to_path_buf()
} else if home_path.exists() && home_path.is_file() {
home_path.to_path_buf()
} else if config_home_non_xdg.exists() && config_home_non_xdg.is_file() {
config_home_non_xdg.to_path_buf()
} else {
return Err(error::Error::NoConfig);
}
};
let content = fs::read_to_string(&config_path).await?;
let toml_content: Config = toml::from_str(&content)?;
Ok((toml_content, config_path))
}
pub async fn save(config_content: &Config, path: PathBuf) -> error::Result<()> {
let mut file = fs::File::create(path).await?;
let content = format!("{}\n", toml::to_string(&config_content)?);
file.write_all(content.as_bytes()).await?;
file.shutdown().await?;
Ok(())
}
pub(crate) fn expand_tilde(s: String) -> error::Result<String> {
if let Some(stripped) = s.strip_prefix('~') {
let home = env::var("HOME")?;
return Ok(home + stripped);
}
Ok(s)
}
fn is_empty_string(s: &str) -> bool {
s.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn loading() {
let config = load(&None).await.unwrap();
assert_eq!(config.1, PathBuf::from("nvrs.toml"));
}
#[tokio::test]
async fn manual_package() {
assert!(
Package::new(
"non_existing_source".to_string(),
"non_existing".to_string(),
false,
String::new()
)
.is_err()
);
assert!(
Package::new(
"github".to_string(),
"orhun/git-cliff".to_string(),
false,
"v".to_string()
)
.is_ok()
);
}
}