From 36db48686de4baf1b122f8b78d96432b398be595 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 22 Apr 2021 06:48:47 +0300 Subject: feat: add struct Tag for git tag name creation --- src/errors.rs | 3 ++ src/lib.rs | 1 + src/tag.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/tag.rs diff --git a/src/errors.rs b/src/errors.rs index 97ed8d8..19a758d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,6 +2,9 @@ use std::path::PathBuf; #[derive(Debug, thiserror::Error)] pub enum BumperError { + #[error("Tag pattern is not ASCII: {0}")] + TagPatternNotAscii(String), + #[error("Can't figure out what kind of project: {0}")] UnknownProjectKind(PathBuf), diff --git a/src/lib.rs b/src/lib.rs index e0c0fca..6285d20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ pub mod git; pub mod project; pub mod python; pub mod rust; +pub mod tag; diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..5a6b4df --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,168 @@ +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"); + } +} -- cgit v1.2.1