use crate::models::{Platform, OS};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PackageDefinition {
Simple(Vec<String>),
Complex(ComplexPackageDefinition),
}
impl PackageDefinition {
pub fn get_sources(&self) -> Vec<&str> {
match self {
PackageDefinition::Simple(sources) => sources.iter().map(|s| s.as_str()).collect(),
PackageDefinition::Complex(complex) => complex.get_sources(),
}
}
pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
match self {
PackageDefinition::Simple(_) => None,
PackageDefinition::Complex(complex) => complex.get_source_config(source),
}
}
pub fn is_available_in(&self, source: &str) -> bool {
self.get_sources().contains(&source)
}
pub fn get_description(&self) -> Option<&str> {
match self {
PackageDefinition::Simple(_) => None,
PackageDefinition::Complex(complex) => complex.description.as_deref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
#[non_exhaustive]
pub struct ComplexPackageDefinition {
#[serde(rename = "_description", skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "_sources", skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<String>>,
#[serde(rename = "_platforms", skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<String>>,
#[serde(rename = "_aliases", skip_serializing_if = "Option::is_none")]
pub aliases: Option<Vec<String>>,
#[serde(flatten)]
pub source_configs: HashMap<String, SourceSpecificConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SourceSpecificConfig {
Name(String),
Complex(SourceConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceConfig {
pub name: Option<String>,
pub pre: Option<String>,
pub post: Option<String>,
pub prefix: Option<String>,
pub install_suffix: Option<String>,
}
pub type SourcesDefinition = HashMap<String, SourceDefinition>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceDefinition {
pub emoji: String,
pub install: String,
pub check: String,
pub prefix: Option<String>,
#[serde(rename = "_overrides", skip_serializing_if = "Option::is_none")]
pub overrides: Option<HashMap<String, PlatformOverride>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformOverride {
pub install: Option<String>,
pub check: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigDefinition {
pub sources: Vec<String>,
pub packages: Vec<String>,
#[serde(rename = "_settings", skip_serializing_if = "Option::is_none")]
pub settings: Option<ConfigSettings>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSettings {
#[serde(default)]
pub auto_update: bool,
#[serde(default = "default_parallel_installs")]
pub parallel_installs: u8,
#[serde(default = "default_true")]
pub confirm_before_install: bool,
}
fn default_parallel_installs() -> u8 {
3
}
fn default_true() -> bool {
true
}
impl ComplexPackageDefinition {
pub fn with_sources(sources: Vec<String>) -> Self {
Self {
sources: Some(sources),
..Default::default()
}
}
pub fn set_platforms(&mut self, platforms: Vec<String>) {
self.platforms = Some(platforms);
}
pub fn set_aliases(&mut self, aliases: Vec<String>) {
self.aliases = Some(aliases);
}
pub fn set_description(&mut self, description: String) {
self.description = Some(description);
}
pub fn get_sources(&self) -> Vec<&str> {
let mut all_sources = Vec::new();
if let Some(sources) = &self.sources {
all_sources.extend(sources.iter().map(|s| s.as_str()));
}
all_sources.extend(self.source_configs.keys().map(|s| s.as_str()));
all_sources
}
pub fn get_source_config(&self, source: &str) -> Option<&SourceSpecificConfig> {
self.source_configs.get(source)
}
pub fn is_available_in(&self, source: &str) -> bool {
self.get_sources().contains(&source)
}
}
impl SourceDefinition {
pub fn get_install_command(&self, platform: &Platform) -> &str {
if let Some(overrides) = &self.overrides {
let platform_key = match platform.os {
OS::Windows => "windows",
OS::Linux => "linux",
OS::Macos => "macos",
};
if let Some(platform_override) = overrides.get(platform_key) {
if let Some(install) = &platform_override.install {
return install;
}
}
}
&self.install
}
pub fn get_check_command(&self, platform: &Platform) -> &str {
if let Some(overrides) = &self.overrides {
let platform_key = match platform.os {
OS::Windows => "windows",
OS::Linux => "linux",
OS::Macos => "macos",
};
if let Some(platform_override) = overrides.get(platform_key) {
if let Some(check) = &platform_override.check {
return check;
}
}
}
&self.check
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_definition_simple_format() {
let ccl = r#"
bat =
= brew
= scoop
= pacman
= nix
"#;
let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
let def = packages.get("bat").unwrap();
assert!(def.is_available_in("brew"));
assert!(def.is_available_in("scoop"));
assert!(def.is_available_in("pacman"));
assert!(def.is_available_in("nix"));
assert!(def.get_source_config("brew").is_none());
let sources = def.get_sources();
assert_eq!(sources.len(), 4);
assert!(sources.contains(&"brew"));
assert!(sources.contains(&"scoop"));
assert!(sources.contains(&"pacman"));
assert!(sources.contains(&"nix"));
}
#[test]
fn test_package_definition_complex_format() {
let ccl = r#"
ripgrep =
brew = gh
_sources =
= scoop
= apt
= pacman
= nix
"#;
let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
let def = packages.get("ripgrep").unwrap();
assert!(def.is_available_in("brew"));
assert!(def.is_available_in("scoop"));
assert!(def.get_source_config("brew").is_some());
let sources = def.get_sources();
assert!(sources.contains(&"scoop"));
assert!(sources.contains(&"apt"));
assert!(sources.contains(&"pacman"));
assert!(sources.contains(&"nix"));
assert!(sources.contains(&"brew"));
}
#[test]
fn test_source_definition() {
let ccl = r#"
emoji = 🍺
install = brew install {package}
check = brew leaves --installed-on-request
"#;
let def: SourceDefinition = sickle::from_str(ccl).unwrap();
assert_eq!(def.emoji, "🍺");
assert!(def.install.contains("{package}"));
}
#[test]
fn test_package_with_description() {
let ccl = r#"
bat =
_description = A cat clone with syntax highlighting.
_sources =
= brew
= scoop
"#;
let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
let def = packages.get("bat").unwrap();
assert_eq!(
def.get_description(),
Some("A cat clone with syntax highlighting.")
);
assert!(def.is_available_in("brew"));
assert!(def.is_available_in("scoop"));
}
#[test]
fn test_simple_package_no_description() {
let ccl = r#"
jq =
= brew
= apt
"#;
let packages: HashMap<String, PackageDefinition> = crate::parse_ccl_to(ccl).unwrap();
let def = packages.get("jq").unwrap();
assert_eq!(def.get_description(), None);
}
}