use crate::errors::BumperError; /// Name git tags after a pattern. /// /// A `Tag` is created from a text template, which can embed the /// following for values to be filled in later: /// /// - `%%` – a single percent character /// - `%n` – the name of the project /// - `%v` – the release version number /// /// The template is parsed by `Tag::new`, which can fail, and the /// values for project name and release version are filled in by /// `Tag::apply`. pub struct Tag { patterns: Vec, } impl Tag { pub fn new(template: &str) -> Result { Ok(Self { patterns: parse_template(template)?, }) } pub fn apply(&self, name: &str, version: &str) -> String { let mut result = String::new(); for p in self.patterns.iter() { match p { Pattern::Fixed(s) => result.push_str(s), Pattern::Name => result.push_str(name), Pattern::Version => result.push_str(version), } } result } } #[derive(Debug)] enum Pattern { Fixed(String), Name, Version, } impl PartialEq for &Pattern { fn eq(&self, other: &Self) -> bool { match self { Pattern::Fixed(s) => match other { Pattern::Fixed(t) => s == t, _ => false, }, Pattern::Name => matches!(other, Pattern::Name), Pattern::Version => matches!(other, Pattern::Version), } } } fn parse_template(t: &str) -> Result, BumperError> { if !t.is_ascii() { return Err(BumperError::TagPatternNotAscii(t.to_string())); } let mut result = vec![]; let mut t = t.to_string(); while !t.is_empty() { let p = if t.starts_with("%%") { t.drain(..2); Pattern::Fixed("%".to_string()) } else if t.starts_with("%v") { t.drain(..2); Pattern::Version } else if t.starts_with("%n") { t.drain(..2); Pattern::Name } else { let c = t.get(..1).unwrap().to_string(); t.drain(..1); Pattern::Fixed(c) }; // Combine fixed string with previous fixed string. if let Pattern::Fixed(b) = &p { if let Some(last) = result.pop() { // There was a previous pattern. Is it a fixed string? if let Pattern::Fixed(a) = last { let mut ab = a.clone(); ab.push_str(b); result.push(Pattern::Fixed(ab)) } else { result.push(last); result.push(p); } } else { // No previous pattern. result.push(p); } } else { result.push(p); }; } Ok(result) } #[cfg(test)] mod test { use super::{parse_template, Pattern, Tag}; fn vecs_eq(this: &[Pattern], that: &[Pattern]) -> bool { println!(); println!("this: {:?}", this); println!("that: {:?}", that); this.iter().eq(that.iter()) } #[test] fn empty_template() { assert!(vecs_eq(&parse_template("").unwrap(), &[])); } #[test] fn fixed_string() { assert!(vecs_eq( &parse_template("foo").unwrap(), &[Pattern::Fixed("foo".to_string())], )); } #[test] fn percent() { assert!(vecs_eq( &parse_template("%%").unwrap(), &[Pattern::Fixed("%".to_string())] )); } #[test] fn version() { assert!(vecs_eq(&parse_template("%v").unwrap(), &[Pattern::Version])); } #[test] fn name() { assert!(vecs_eq(&parse_template("%n").unwrap(), &[Pattern::Name])); } #[test] fn many_parts() { assert!(vecs_eq( &parse_template("this-is-a-%n-%v-RELEASE").unwrap(), &[ Pattern::Fixed("this-is-a-".to_string()), Pattern::Name, Pattern::Fixed("-".to_string()), Pattern::Version, Pattern::Fixed("-RELEASE".to_string()) ] )); } #[test] fn apply() { let tag = Tag::new("%n-%v").unwrap(); assert_eq!(tag.apply("foo", "1.2.3"), "foo-1.2.3"); } }