bump-bin 0.4.3

Increments version with semver specification
Documentation
use crate::fs::{read_file, read_from_stdin};
use anyhow::Result;
use anyhow::{anyhow, bail, Error};

use log::*;
use once_cell::sync::Lazy;
use regex::Regex;
use semver::{BuildMetadata, Prerelease, Version as SemVer};
use std::fmt;
use std::io::IsTerminal;
use std::str::FromStr;

static PREFIX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^(?P<prefix>.*?)(?P<version>[0-9]+?.[0-9]+?.[0-9]+?(?:.*)$)").unwrap()
});

#[derive(PartialEq, Debug, Clone)]
pub struct Version {
    prefix: String,
    ver: SemVer,
}

impl FromStr for Version {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let caps = PREFIX
            .captures(s)
            .ok_or_else(|| anyhow!("Can't find semver format. value: {}", s))?;

        let cap_pre = caps.name("prefix");
        let cap_ver = caps.name("version");

        let (prefix, version) = match (cap_pre, cap_ver) {
            (Some(p), Some(v)) => (p.as_str(), v.as_str()),
            (None, Some(v)) => ("", v.as_str()),
            _ => bail!("Can't find semver format. value: {}", s),
        };

        debug!("prefix: {}", prefix);
        debug!("version: {}", version);

        Ok(Version {
            prefix: prefix.to_string(),
            ver: SemVer::parse(version)?,
        })
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.prefix, self.ver)
    }
}

impl TryFrom<(Option<String>, Option<String>)> for Version {
    type Error = Error;

    fn try_from(value: (Option<String>, Option<String>)) -> std::result::Result<Self, Self::Error> {
        let v = match value {
            (Some(ref ver), None) if ver == "-" => {
                let buf = read_from_stdin()?;
                Version::from_str(buf.trim_end())?
            }
            (None, None) if !std::io::stdin().is_terminal() => {
                let buf = read_from_stdin()?;
                Version::from_str(buf.trim_end())?
            }
            (None, Some(ref ver)) => Version::from_str(ver)?,
            (Some(_), Some(_)) => {
                bail!("Invalid argument, specify either ver args or file option")
            }
            (Some(f), _) => {
                let buf = String::from_utf8(read_file(f)?)?;
                Version::from_str(buf.trim_end())?
            }
            _ => bail!("Invalid argument"),
        };
        Ok(v)
    }
}

impl Version {
    pub fn bump_patch(&self) -> Version {
        let mut v = self.clone();
        v.ver.patch += 1;
        v.ver.pre = Prerelease::EMPTY;
        v.ver.build = BuildMetadata::EMPTY;
        v
    }

    pub fn bump_minor(&self) -> Version {
        let mut v = self.clone();
        v.ver.minor += 1;
        v.ver.patch = 0;
        v.ver.pre = Prerelease::EMPTY;
        v.ver.build = BuildMetadata::EMPTY;
        v
    }

    pub fn bump_major(&self) -> Version {
        let mut v = self.clone();
        v.ver.major += 1;
        v.ver.minor = 0;
        v.ver.patch = 0;
        v.ver.pre = Prerelease::EMPTY;
        v.ver.build = BuildMetadata::EMPTY;
        v
    }

    pub fn update_pre_release(&self, pre: impl Into<String>) -> Result<Version> {
        let mut v = self.clone();
        v.ver.pre = Prerelease::new(pre.into().as_str())?;
        Ok(v)
    }

    pub fn update_build(&self, build: impl Into<String>) -> Result<Version> {
        let mut v = self.clone();
        v.ver.build = BuildMetadata::new(build.into().as_str())?;
        Ok(v)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_str_ok() {
        let result = Version::from_str("0.0.0");
        assert!(result.is_ok());
    }

    #[test]
    fn from_str_ng() {
        let inputs = vec!["x.x.x", "vx.x.x", "x", "x.x"];
        for input in inputs {
            let result = Version::from_str(input);
            assert!(result.is_err());
            let msg = result.expect_err("").to_string();
            assert!(msg.contains("Can't find semver format"));
        }
    }

    #[test]
    fn try_from_ok() {
        let result = Version::try_from((None, Some(String::from("0.0.0"))));

        let prefix = String::from("");
        let ver = SemVer::new(0, 0, 0);
        let _version = Version { prefix, ver };
        assert!(matches!(result, Ok(_version)));
    }

    #[test]
    fn try_from_ng() {
        let inputs = vec![
            (Some(String::from("test.md")), Some(String::from("x.x.x"))),
            (None, None),
        ];
        for (file, ver) in inputs {
            let result = Version::try_from((file, ver));
            assert!(matches!(result, Err(_)));
        }
    }
}