use serde::Deserialize;
use vein_adapter::DelayPolicy as AdapterDelayPolicy;
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct DelayPolicyConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "DelayPolicyConfig::default_delay_days")]
pub default_delay_days: u32,
#[serde(default = "DelayPolicyConfig::default_skip_weekends")]
pub skip_weekends: bool,
#[serde(default = "DelayPolicyConfig::default_business_hours_only")]
pub business_hours_only: bool,
#[serde(default = "DelayPolicyConfig::default_release_hour_utc")]
pub release_hour_utc: u8,
#[serde(default)]
pub gems: Vec<GemDelayOverride>,
#[serde(default)]
pub pinned: Vec<PinnedVersion>,
}
impl DelayPolicyConfig {
fn default_delay_days() -> u32 {
3
}
fn default_skip_weekends() -> bool {
true
}
fn default_business_hours_only() -> bool {
true
}
fn default_release_hour_utc() -> u8 {
9
}
pub fn to_adapter_policy(&self) -> AdapterDelayPolicy {
AdapterDelayPolicy {
default_delay_days: self.default_delay_days,
skip_weekends: self.skip_weekends,
business_hours_only: self.business_hours_only,
release_hour_utc: self.release_hour_utc,
}
}
pub fn delay_for_gem(&self, name: &str) -> u32 {
for override_config in &self.gems {
if override_config.pattern {
if glob_match(&override_config.name, name) {
return override_config.delay_days;
}
} else if override_config.name == name {
return override_config.delay_days;
}
}
self.default_delay_days
}
pub fn is_pinned(&self, name: &str, version: &str) -> bool {
self.pinned
.iter()
.any(|p| p.name == name && p.version == version)
}
pub fn pin_reason(&self, name: &str, version: &str) -> Option<&str> {
self.pinned
.iter()
.find(|p| p.name == name && p.version == version)
.map(|p| p.reason.as_str())
}
}
impl Default for DelayPolicyConfig {
fn default() -> Self {
Self {
enabled: false, default_delay_days: Self::default_delay_days(),
skip_weekends: Self::default_skip_weekends(),
business_hours_only: Self::default_business_hours_only(),
release_hour_utc: Self::default_release_hour_utc(),
gems: Vec::new(),
pinned: Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct GemDelayOverride {
pub name: String,
pub delay_days: u32,
#[serde(default)]
pub pattern: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PinnedVersion {
pub name: String,
pub version: String,
pub reason: String,
}
fn glob_match(pattern: &str, name: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(suffix) = pattern.strip_prefix('*') {
return name.ends_with(suffix);
}
if let Some(prefix) = pattern.strip_suffix('*') {
return name.starts_with(prefix);
}
if let Some(pos) = pattern.find('*') {
let prefix = &pattern[..pos];
let suffix = &pattern[pos + 1..];
return name.starts_with(prefix) && name.ends_with(suffix);
}
pattern == name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DelayPolicyConfig::default();
assert!(!config.enabled);
assert_eq!(config.default_delay_days, 3);
assert!(config.skip_weekends);
assert!(config.business_hours_only);
assert_eq!(config.release_hour_utc, 9);
}
#[test]
fn test_delay_for_gem_default() {
let config = DelayPolicyConfig::default();
assert_eq!(config.delay_for_gem("rails"), 3);
}
#[test]
fn test_delay_for_gem_override() {
let config = DelayPolicyConfig {
gems: vec![GemDelayOverride {
name: "rails".to_string(),
delay_days: 7,
pattern: false,
}],
..Default::default()
};
assert_eq!(config.delay_for_gem("rails"), 7);
assert_eq!(config.delay_for_gem("rack"), 3);
}
#[test]
fn test_delay_for_gem_pattern() {
let config = DelayPolicyConfig {
gems: vec![GemDelayOverride {
name: "*-internal".to_string(),
delay_days: 0,
pattern: true,
}],
..Default::default()
};
assert_eq!(config.delay_for_gem("my-gem-internal"), 0);
assert_eq!(config.delay_for_gem("rails"), 3);
}
#[test]
fn test_is_pinned() {
let config = DelayPolicyConfig {
pinned: vec![PinnedVersion {
name: "nokogiri".to_string(),
version: "1.16.0".to_string(),
reason: "CVE-2024-XXXXX".to_string(),
}],
..Default::default()
};
assert!(config.is_pinned("nokogiri", "1.16.0"));
assert!(!config.is_pinned("nokogiri", "1.15.0"));
assert!(!config.is_pinned("rails", "7.0.0"));
}
#[test]
fn test_glob_match() {
assert!(glob_match("*-internal", "my-gem-internal"));
assert!(!glob_match("*-internal", "internal-gem"));
assert!(glob_match("rails-*", "rails-api"));
assert!(!glob_match("rails-*", "my-rails"));
assert!(glob_match("my-*-gem", "my-awesome-gem"));
assert!(!glob_match("my-*-gem", "your-awesome-gem"));
assert!(glob_match("rails", "rails"));
assert!(!glob_match("rails", "rack"));
assert!(glob_match("*", "anything"));
}
#[test]
fn test_toml_parsing() {
let toml = r#"
enabled = true
default_delay_days = 5
skip_weekends = false
business_hours_only = true
release_hour_utc = 14
[[gems]]
name = "rails"
delay_days = 7
[[gems]]
name = "*-internal"
delay_days = 0
pattern = true
[[pinned]]
name = "nokogiri"
version = "1.16.0"
reason = "CVE-2024-XXXXX"
"#;
let config: DelayPolicyConfig = toml::from_str(toml).unwrap();
assert!(config.enabled);
assert_eq!(config.default_delay_days, 5);
assert!(!config.skip_weekends);
assert_eq!(config.release_hour_utc, 14);
assert_eq!(config.gems.len(), 2);
assert_eq!(config.pinned.len(), 1);
}
}