use crate::types::ParseError as TypesParseError;
use crate::VersionPolicy;
use deb822_lossless::{Deb822, Paragraph};
use std::str::FromStr;
fn watch_option_to_key(option: &crate::types::WatchOption) -> &'static str {
use crate::types::WatchOption;
match option {
WatchOption::Component(_) => "Component",
WatchOption::Compression(_) => "Compression",
WatchOption::UserAgent(_) => "User-Agent",
WatchOption::Pagemangle(_) => "Pagemangle",
WatchOption::Uversionmangle(_) => "Uversionmangle",
WatchOption::Dversionmangle(_) => "Dversionmangle",
WatchOption::Dirversionmangle(_) => "Dirversionmangle",
WatchOption::Oversionmangle(_) => "Oversionmangle",
WatchOption::Downloadurlmangle(_) => "Downloadurlmangle",
WatchOption::Pgpsigurlmangle(_) => "Pgpsigurlmangle",
WatchOption::Filenamemangle(_) => "Filenamemangle",
WatchOption::VersionPolicy(_) => "Version-Policy",
WatchOption::Searchmode(_) => "Searchmode",
WatchOption::Mode(_) => "Mode",
WatchOption::Pgpmode(_) => "Pgpmode",
WatchOption::Gitexport(_) => "Gitexport",
WatchOption::Gitmode(_) => "Gitmode",
WatchOption::Pretty(_) => "Pretty",
WatchOption::Ctype(_) => "Ctype",
WatchOption::Repacksuffix(_) => "Repacksuffix",
WatchOption::Unzipopt(_) => "Unzipopt",
WatchOption::Script(_) => "Script",
WatchOption::Decompress => "Decompress",
WatchOption::Bare => "Bare",
WatchOption::Repack => "Repack",
}
}
#[derive(Debug)]
pub struct ParseError(String);
impl std::error::Error for ParseError {}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "ParseError: {}", self.0)
}
}
#[derive(Debug)]
pub struct WatchFile(Deb822);
#[derive(Debug)]
pub struct Entry {
paragraph: Paragraph,
defaults: Option<Paragraph>,
}
impl WatchFile {
pub fn as_deb822(&self) -> &Deb822 {
&self.0
}
pub fn new() -> Self {
let content = "Version: 5\n";
WatchFile::from_str(content).expect("Failed to create empty watch file")
}
pub fn version(&self) -> u32 {
5
}
pub fn defaults(&self) -> Option<Paragraph> {
let paragraphs: Vec<_> = self.0.paragraphs().collect();
if paragraphs.len() > 1 {
if !paragraphs[1].contains_key("Source") && !paragraphs[1].contains_key("source") {
return Some(paragraphs[1].clone());
}
}
None
}
pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
let paragraphs: Vec<_> = self.0.paragraphs().collect();
let defaults = self.defaults();
let start_index = if paragraphs.len() > 1 {
let has_source =
paragraphs[1].contains_key("Source") || paragraphs[1].contains_key("source");
let has_template =
paragraphs[1].contains_key("Template") || paragraphs[1].contains_key("template");
if !has_source && !has_template {
2 } else {
1 }
} else {
1
};
paragraphs
.into_iter()
.skip(start_index)
.map(move |p| Entry {
paragraph: p,
defaults: defaults.clone(),
})
}
pub fn inner(&self) -> &Deb822 {
&self.0
}
pub fn inner_mut(&mut self) -> &mut Deb822 {
&mut self.0
}
pub fn add_entry(&mut self, source: &str, matching_pattern: &str) -> Entry {
let mut para = self.0.add_paragraph();
para.set("Source", source);
para.set("Matching-Pattern", matching_pattern);
let defaults = self.defaults();
Entry {
paragraph: para.clone(),
defaults,
}
}
}
impl Default for WatchFile {
fn default() -> Self {
Self::new()
}
}
impl FromStr for WatchFile {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match Deb822::from_str(s) {
Ok(deb822) => {
let version = deb822
.paragraphs()
.next()
.and_then(|p| p.get("Version"))
.unwrap_or_else(|| "1".to_string());
if version != "5" {
return Err(ParseError(format!("Expected version 5, got {}", version)));
}
Ok(WatchFile(deb822))
}
Err(e) => Err(ParseError(e.to_string())),
}
}
}
impl std::fmt::Display for WatchFile {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Entry {
pub(crate) fn get_field(&self, key: &str) -> Option<String> {
if let Some(value) = self.paragraph.get(key) {
return Some(value);
}
let normalized_key = normalize_key(key);
for (k, v) in self.paragraph.items() {
if normalize_key(&k) == normalized_key {
return Some(v);
}
}
if let Some(ref defaults) = self.defaults {
if let Some(value) = defaults.get(key) {
return Some(value);
}
for (k, v) in defaults.items() {
if normalize_key(&k) == normalized_key {
return Some(v);
}
}
}
None
}
pub fn source(&self) -> Result<Option<String>, crate::templates::TemplateError> {
if let Some(source) = self.get_field("Source") {
return Ok(Some(source));
}
if self.get_field("Template").is_none() {
return Ok(None);
}
self.expand_template().map(|t| t.source)
}
pub fn matching_pattern(&self) -> Result<Option<String>, crate::templates::TemplateError> {
if let Some(pattern) = self.get_field("Matching-Pattern") {
return Ok(Some(pattern));
}
if self.get_field("Template").is_none() {
return Ok(None);
}
self.expand_template().map(|t| t.matching_pattern)
}
pub fn as_deb822(&self) -> &Paragraph {
&self.paragraph
}
pub fn component(&self) -> Option<String> {
self.get_field("Component")
}
pub fn get_option(&self, key: &str) -> Option<String> {
match key {
"Source" => None, "Matching-Pattern" => None, "Component" => None, "Version" => None, key => self.get_field(key),
}
}
pub fn set_option(&mut self, option: crate::types::WatchOption) {
use crate::types::WatchOption;
let (key, value) = match option {
WatchOption::Component(v) => ("Component", Some(v)),
WatchOption::Compression(v) => ("Compression", Some(v.to_string())),
WatchOption::UserAgent(v) => ("User-Agent", Some(v)),
WatchOption::Pagemangle(v) => ("Pagemangle", Some(v)),
WatchOption::Uversionmangle(v) => ("Uversionmangle", Some(v)),
WatchOption::Dversionmangle(v) => ("Dversionmangle", Some(v)),
WatchOption::Dirversionmangle(v) => ("Dirversionmangle", Some(v)),
WatchOption::Oversionmangle(v) => ("Oversionmangle", Some(v)),
WatchOption::Downloadurlmangle(v) => ("Downloadurlmangle", Some(v)),
WatchOption::Pgpsigurlmangle(v) => ("Pgpsigurlmangle", Some(v)),
WatchOption::Filenamemangle(v) => ("Filenamemangle", Some(v)),
WatchOption::VersionPolicy(v) => ("Version-Policy", Some(v.to_string())),
WatchOption::Searchmode(v) => ("Searchmode", Some(v.to_string())),
WatchOption::Mode(v) => ("Mode", Some(v.to_string())),
WatchOption::Pgpmode(v) => ("Pgpmode", Some(v.to_string())),
WatchOption::Gitexport(v) => ("Gitexport", Some(v.to_string())),
WatchOption::Gitmode(v) => ("Gitmode", Some(v.to_string())),
WatchOption::Pretty(v) => ("Pretty", Some(v.to_string())),
WatchOption::Ctype(v) => ("Ctype", Some(v.to_string())),
WatchOption::Repacksuffix(v) => ("Repacksuffix", Some(v)),
WatchOption::Unzipopt(v) => ("Unzipopt", Some(v)),
WatchOption::Script(v) => ("Script", Some(v)),
WatchOption::Decompress => ("Decompress", None),
WatchOption::Bare => ("Bare", None),
WatchOption::Repack => ("Repack", None),
};
if let Some(v) = value {
self.paragraph.set(key, &v);
} else {
self.paragraph.set(key, "");
}
}
pub fn set_option_str(&mut self, key: &str, value: &str) {
self.paragraph.set(key, value);
}
pub fn delete_option(&mut self, option: crate::types::WatchOption) {
let key = watch_option_to_key(&option);
self.paragraph.remove(key);
}
pub fn delete_option_str(&mut self, key: &str) {
self.paragraph.remove(key);
}
pub fn url(&self) -> String {
self.source().unwrap_or(None).unwrap_or_default()
}
pub fn version_policy(&self) -> Result<Option<VersionPolicy>, TypesParseError> {
match self.get_field("Version-Policy") {
Some(policy) => Ok(Some(policy.parse()?)),
None => Ok(None),
}
}
pub fn script(&self) -> Option<String> {
self.get_field("Script")
}
pub fn set_source(&mut self, url: &str) {
self.paragraph.set("Source", url);
}
pub fn set_matching_pattern(&mut self, pattern: &str) {
self.paragraph.set("Matching-Pattern", pattern);
}
pub fn line(&self) -> usize {
self.paragraph.line()
}
pub fn mode(&self) -> Result<crate::types::Mode, TypesParseError> {
Ok(self
.get_field("Mode")
.map(|s| s.parse())
.transpose()?
.unwrap_or_default())
}
fn expand_template(
&self,
) -> Result<crate::templates::ExpandedTemplate, crate::templates::TemplateError> {
use crate::templates::{expand_template, parse_github_url, Template, TemplateError};
let template_str =
self.get_field("Template")
.ok_or_else(|| TemplateError::MissingField {
template: "any".to_string(),
field: "Template".to_string(),
})?;
let release_only = self
.get_field("Release-Only")
.map(|v| v.to_lowercase() == "yes")
.unwrap_or(false);
let version_type = self.get_field("Version-Type");
let template = match template_str.to_lowercase().as_str() {
"github" => {
let (owner, repository) = if let (Some(o), Some(p)) =
(self.get_field("Owner"), self.get_field("Project"))
{
(o, p)
} else if let Some(dist) = self.get_field("Dist") {
parse_github_url(&dist)?
} else {
return Err(TemplateError::MissingField {
template: "GitHub".to_string(),
field: "Dist or Owner+Project".to_string(),
});
};
Template::GitHub {
owner,
repository,
release_only,
version_type,
}
}
"gitlab" => {
let dist = self
.get_field("Dist")
.ok_or_else(|| TemplateError::MissingField {
template: "GitLab".to_string(),
field: "Dist".to_string(),
})?;
Template::GitLab {
dist,
release_only,
version_type,
}
}
"pypi" => {
let package =
self.get_field("Dist")
.ok_or_else(|| TemplateError::MissingField {
template: "PyPI".to_string(),
field: "Dist".to_string(),
})?;
Template::PyPI {
package,
version_type,
}
}
"npmregistry" => {
let package =
self.get_field("Dist")
.ok_or_else(|| TemplateError::MissingField {
template: "Npmregistry".to_string(),
field: "Dist".to_string(),
})?;
Template::Npmregistry {
package,
version_type,
}
}
"metacpan" => {
let dist = self
.get_field("Dist")
.ok_or_else(|| TemplateError::MissingField {
template: "Metacpan".to_string(),
field: "Dist".to_string(),
})?;
Template::Metacpan { dist, version_type }
}
_ => return Err(TemplateError::UnknownTemplate(template_str)),
};
Ok(expand_template(template))
}
pub fn try_convert_to_template(&mut self) -> Option<crate::templates::Template> {
use crate::templates::detect_template;
let source = self.source().ok().flatten();
let matching_pattern = self.matching_pattern().ok().flatten();
let searchmode = self.get_field("Searchmode");
let mode = self.get_field("Mode");
let template = detect_template(
source.as_deref(),
matching_pattern.as_deref(),
searchmode.as_deref(),
mode.as_deref(),
)?;
self.paragraph.remove("Source");
self.paragraph.remove("Matching-Pattern");
self.paragraph.remove("Searchmode");
self.paragraph.remove("Mode");
match &template {
crate::templates::Template::GitHub {
owner,
repository,
release_only,
version_type,
} => {
self.paragraph.set("Template", "GitHub");
self.paragraph.set("Owner", owner);
self.paragraph.set("Project", repository);
if *release_only {
self.paragraph.set("Release-Only", "yes");
}
if let Some(vt) = version_type {
self.paragraph.set("Version-Type", vt);
}
}
crate::templates::Template::GitLab {
dist,
release_only: _,
version_type,
} => {
self.paragraph.set("Template", "GitLab");
self.paragraph.set("Dist", dist);
if let Some(vt) = version_type {
self.paragraph.set("Version-Type", vt);
}
}
crate::templates::Template::PyPI {
package,
version_type,
} => {
self.paragraph.set("Template", "PyPI");
self.paragraph.set("Dist", package);
if let Some(vt) = version_type {
self.paragraph.set("Version-Type", vt);
}
}
crate::templates::Template::Npmregistry {
package,
version_type,
} => {
self.paragraph.set("Template", "Npmregistry");
self.paragraph.set("Dist", package);
if let Some(vt) = version_type {
self.paragraph.set("Version-Type", vt);
}
}
crate::templates::Template::Metacpan { dist, version_type } => {
self.paragraph.set("Template", "Metacpan");
self.paragraph.set("Dist", dist);
if let Some(vt) = version_type {
self.paragraph.set("Version-Type", vt);
}
}
}
Some(template)
}
}
fn normalize_key(key: &str) -> String {
key.to_lowercase().replace(['-', '_'], "")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_as_deb822() {
let input = r#"Version: 5
Source: https://github.com/owner/repo/tags
Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let deb822 = wf.as_deb822();
assert_eq!(deb822.paragraphs().count(), 2);
}
#[test]
fn test_create_v5_watchfile() {
let wf = WatchFile::new();
assert_eq!(wf.version(), 5);
let output = wf.to_string();
assert!(output.contains("Version"));
assert!(output.contains("5"));
}
#[test]
fn test_parse_v5_basic() {
let input = r#"Version: 5
Source: https://github.com/owner/repo/tags
Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
assert_eq!(wf.version(), 5);
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap().as_deref(),
Some("https://github.com/owner/repo/tags")
);
assert_eq!(
entry.matching_pattern().unwrap(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".to_string())
);
}
#[test]
fn test_parse_v5_multiple_entries() {
let input = r#"Version: 5
Source: https://github.com/owner/repo1/tags
Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
Source: https://github.com/owner/repo2/tags
Matching-Pattern: .*/release-(\d\S+)\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0].source().unwrap().as_deref(),
Some("https://github.com/owner/repo1/tags")
);
assert_eq!(
entries[1].source().unwrap().as_deref(),
Some("https://github.com/owner/repo2/tags")
);
}
#[test]
fn test_v5_case_insensitive_fields() {
let input = r#"Version: 5
source: https://example.com/files
matching-pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap().as_deref(),
Some("https://example.com/files")
);
assert_eq!(
entry.matching_pattern().unwrap().as_deref(),
Some(".*\\.tar\\.gz")
);
}
#[test]
fn test_v5_with_compression_option() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Compression: xz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let compression = entry.get_option("compression");
assert!(compression.is_some());
}
#[test]
fn test_v5_with_component() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Component: foo
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.component(), Some("foo".to_string()));
}
#[test]
fn test_v5_rejects_wrong_version() {
let input = r#"Version: 4
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
"#;
let result: Result<WatchFile, _> = input.parse();
assert!(result.is_err());
}
#[test]
fn test_v5_roundtrip() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let output = wf.to_string();
let wf2: WatchFile = output.parse().unwrap();
assert_eq!(wf2.version(), 5);
let entries: Vec<_> = wf2.entries().collect();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_normalize_key() {
assert_eq!(normalize_key("Matching-Pattern"), "matchingpattern");
assert_eq!(normalize_key("matching_pattern"), "matchingpattern");
assert_eq!(normalize_key("MatchingPattern"), "matchingpattern");
assert_eq!(normalize_key("MATCHING-PATTERN"), "matchingpattern");
}
#[test]
fn test_defaults_paragraph() {
let input = r#"Version: 5
Compression: xz
User-Agent: Custom/1.0
Source: https://example.com/repo1
Matching-Pattern: .*\.tar\.gz
Source: https://example.com/repo2
Matching-Pattern: .*\.tar\.gz
Compression: gz
"#;
let wf: WatchFile = input.parse().unwrap();
let defaults = wf.defaults();
assert!(defaults.is_some());
let defaults = defaults.unwrap();
assert_eq!(defaults.get("Compression"), Some("xz".to_string()));
assert_eq!(defaults.get("User-Agent"), Some("Custom/1.0".to_string()));
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
assert_eq!(
entries[0].get_option("User-Agent"),
Some("Custom/1.0".to_string())
);
assert_eq!(entries[1].get_option("Compression"), Some("gz".to_string()));
assert_eq!(
entries[1].get_option("User-Agent"),
Some("Custom/1.0".to_string())
);
}
#[test]
fn test_no_defaults_paragraph() {
let input = r#"Version: 5
Source: https://example.com/repo1
Matching-Pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
assert!(wf.defaults().is_none());
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_set_source() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
assert_eq!(
entry.source().unwrap(),
Some("https://example.com/repo1".to_string())
);
entry.set_source("https://example.com/repo2");
assert_eq!(
entry.source().unwrap(),
Some("https://example.com/repo2".to_string())
);
}
#[test]
fn test_set_matching_pattern() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
assert_eq!(
entry.matching_pattern().unwrap(),
Some(".*\\.tar\\.gz".to_string())
);
entry.set_matching_pattern(".*/v?([\\d.]+)\\.tar\\.gz");
assert_eq!(
entry.matching_pattern().unwrap(),
Some(".*/v?([\\d.]+)\\.tar\\.gz".to_string())
);
}
#[test]
fn test_entry_line() {
let input = r#"Version: 5
Source: https://example.com/repo1
Matching-Pattern: .*\.tar\.gz
Source: https://example.com/repo2
Matching-Pattern: .*\.tar\.xz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries[0].line(), 2);
assert_eq!(entries[1].line(), 5);
}
#[test]
fn test_defaults_with_case_variations() {
let input = r#"Version: 5
compression: xz
user-agent: Custom/1.0
Source: https://example.com/repo1
Matching-Pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
assert_eq!(
entries[0].get_option("User-Agent"),
Some("Custom/1.0".to_string())
);
}
#[test]
fn test_v5_with_uversionmangle() {
let input = r#"Version: 5
Source: https://pypi.org/project/foo/
Matching-Pattern: foo-(\d+\.\d+)\.tar\.gz
Uversionmangle: s/\.0+$//
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.get_option("Uversionmangle"),
Some("s/\\.0+$//".to_string())
);
}
#[test]
fn test_v5_with_filenamemangle() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Filenamemangle: s/.*\///;s/@PACKAGE@-(.*)\.tar\.gz/foo_$1.orig.tar.gz/
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.get_option("Filenamemangle"),
Some("s/.*\\///;s/@PACKAGE@-(.*)\\.tar\\.gz/foo_$1.orig.tar.gz/".to_string())
);
}
#[test]
fn test_v5_with_searchmode() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: foo-(\d[\d.]*)\.tar\.gz
Searchmode: plain
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.get_field("Searchmode").as_deref(), Some("plain"));
}
#[test]
fn test_v5_with_version_policy() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Version-Policy: debian
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
let policy = entry.version_policy();
assert!(policy.is_ok());
assert_eq!(format!("{:?}", policy.unwrap().unwrap()), "Debian");
}
#[test]
fn test_v5_multiple_mangles() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Uversionmangle: s/^v//;s/\.0+$//
Dversionmangle: s/\+dfsg\d*$//
Filenamemangle: s/.*/foo-$1.tar.gz/
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.get_option("Uversionmangle"),
Some("s/^v//;s/\\.0+$//".to_string())
);
assert_eq!(
entry.get_option("Dversionmangle"),
Some("s/\\+dfsg\\d*$//".to_string())
);
assert_eq!(
entry.get_option("Filenamemangle"),
Some("s/.*/foo-$1.tar.gz/".to_string())
);
}
#[test]
fn test_v5_with_pgpmode() {
let input = r#"Version: 5
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
Pgpmode: auto
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(entry.get_option("Pgpmode"), Some("auto".to_string()));
}
#[test]
fn test_v5_with_comments() {
let input = r#"Version: 5
# This is a comment about the entry
Source: https://example.com/files
Matching-Pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let output = wf.to_string();
assert!(output.contains("# This is a comment about the entry"));
}
#[test]
fn test_v5_empty_after_version() {
let input = "Version: 5\n";
let wf: WatchFile = input.parse().unwrap();
assert_eq!(wf.version(), 5);
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 0);
}
#[test]
fn test_v5_trait_url() {
let input = r#"Version: 5
Source: https://example.com/files/@PACKAGE@
Matching-Pattern: .*\.tar\.gz
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap().as_deref(),
Some("https://example.com/files/@PACKAGE@")
);
}
#[test]
fn test_github_template() {
let input = r#"Version: 5
Template: GitHub
Owner: torvalds
Project: linux
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap(),
Some("https://github.com/torvalds/linux/tags".to_string())
);
assert_eq!(
entry.matching_pattern().unwrap(),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
);
}
#[test]
fn test_github_template_with_dist() {
let input = r#"Version: 5
Template: GitHub
Dist: https://github.com/guimard/llng-docker
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap(),
Some("https://github.com/guimard/llng-docker/tags".to_string())
);
}
#[test]
fn test_pypi_template() {
let input = r#"Version: 5
Template: PyPI
Dist: bitbox02
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap(),
Some("https://pypi.debian.net/bitbox02/".to_string())
);
assert_eq!(
entry.matching_pattern().unwrap(),
Some(
r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"
.to_string()
)
);
}
#[test]
fn test_gitlab_template() {
let input = r#"Version: 5
Template: GitLab
Dist: https://salsa.debian.org/debian/devscripts
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap(),
Some("https://salsa.debian.org/debian/devscripts".to_string())
);
assert_eq!(
entry.matching_pattern().unwrap(),
Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
);
}
#[test]
fn test_template_with_explicit_source() {
let input = r#"Version: 5
Template: GitHub
Owner: test
Project: project
Source: https://custom.example.com/
"#;
let wf: WatchFile = input.parse().unwrap();
let entries: Vec<_> = wf.entries().collect();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.source().unwrap(),
Some("https://custom.example.com/".to_string())
);
}
#[test]
fn test_convert_to_template_github() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry(
"https://github.com/torvalds/linux/tags",
r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
);
entry.set_option_str("Searchmode", "html");
let template = entry.try_convert_to_template();
assert_eq!(
template,
Some(crate::templates::Template::GitHub {
owner: "torvalds".to_string(),
repository: "linux".to_string(),
release_only: false,
version_type: None,
})
);
assert_eq!(entry.get_field("Template"), Some("GitHub".to_string()));
assert_eq!(entry.get_field("Owner"), Some("torvalds".to_string()));
assert_eq!(entry.get_field("Project"), Some("linux".to_string()));
assert_eq!(entry.get_field("Source"), None);
assert_eq!(entry.get_field("Matching-Pattern"), None);
}
#[test]
fn test_convert_to_template_pypi() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry(
"https://pypi.debian.net/bitbox02/",
r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz",
);
entry.set_option_str("Searchmode", "plain");
let template = entry.try_convert_to_template();
assert_eq!(
template,
Some(crate::templates::Template::PyPI {
package: "bitbox02".to_string(),
version_type: None,
})
);
assert_eq!(entry.get_field("Template"), Some("PyPI".to_string()));
assert_eq!(entry.get_field("Dist"), Some("bitbox02".to_string()));
}
#[test]
fn test_convert_to_template_no_match() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry(
"https://example.com/downloads/",
r".*/v?(\d+\.\d+)\.tar\.gz",
);
let template = entry.try_convert_to_template();
assert_eq!(template, None);
assert_eq!(
entry.source().unwrap(),
Some("https://example.com/downloads/".to_string())
);
}
#[test]
fn test_convert_to_template_roundtrip() {
let mut wf = WatchFile::new();
let mut entry = wf.add_entry(
"https://github.com/test/project/releases",
r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
);
entry.set_option_str("Searchmode", "html");
entry.try_convert_to_template().unwrap();
let source = entry.source().unwrap();
let matching_pattern = entry.matching_pattern().unwrap();
assert_eq!(
source,
Some("https://github.com/test/project/releases".to_string())
);
assert_eq!(
matching_pattern,
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
);
}
}