summaryrefslogtreecommitdiff
path: root/src/tag.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tag.rs')
-rw-r--r--src/tag.rs168
1 files changed, 168 insertions, 0 deletions
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<Pattern>,
+}
+
+impl Tag {
+ pub fn new(template: &str) -> Result<Self, BumperError> {
+ 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<Vec<Pattern>, 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");
+ }
+}