cargo-ver 0.2.3

A cargo extension to manage crate versions.
Documentation
use std::{
	env,
	error::Error,
	fs,
	path::PathBuf,
};

use toml_edit::{
	Document,
	Item,
};

use crate::{
	app,
	Version,
	VersionField,
};

enum Command {
	Set(Version),
	Get,
	Bump(VersionField),
}

pub struct Cmd {
	cmd: Command,
	toml_path: PathBuf,
}

impl Cmd {
	pub fn from_args() -> Result<Self, Box<dyn Error>> {
		let args = env::args().skip(1).collect::<Vec<_>>();
		if args.is_empty() {
			return Err("this command should be invoked from cargo (cargo ver)".into());
		}
		let matches = app::new().get_matches_from(args);
		let toml_path = match matches.value_of("toml").map(PathBuf::from) {
			Some(p) => p,
			None => find_toml()?,
		};

		Ok(match matches.subcommand_name() {
			None => Self {
				toml_path,
				cmd: Command::Get,
			},
			Some("bump") => {
				let m = matches.subcommand_matches("bump").unwrap();
				let field = match &m.value_of("field").unwrap().to_lowercase()[..] {
					"p" | "pat" | "patch" => VersionField::Patch,
					"min" | "minor" => VersionField::Minor,
					"maj" | "major" => VersionField::Major,
					s => panic!("unhandled field in match expression: {:?}", s),
				};
				Self {
					toml_path,
					cmd: Command::Bump(field),
				}
			}
			Some("set") => {
				let m = matches.subcommand_matches("set").unwrap();
				let val = m.value_of("version").unwrap().parse::<Version>()?;
				Self {
					toml_path,
					cmd: Command::Set(val),
				}
			}
			s => panic!("unhandled match case for subcommands: {:?}", s),
		})
	}

	pub fn run(self) -> Result<(), Box<dyn Error>> {
		let Self { toml_path, cmd } = self;

		let data = fs::read_to_string(&toml_path)?;

		let mut man = data.parse::<Document>()?;
		let old_version_str = match &mut man["package"] {
			Item::None => {
				return Err(format!(
					"the manifest at {} does not contain a [package] section",
					toml_path.display()
				)
				.into())
			}
			Item::Table(pkg) => match pkg.entry("version") {
				Item::None => return Err("the crate manifest is missing the version field".into()),
				Item::Value(v) if v.is_str() => v,
				_ => {
					return Err(
						"the crate manifests `[package].version] field must be a string".into(),
					)
				}
			},
			_ => {
				return Err(format!(
					"unrecognized manifest at {}: the `package` must be a table",
					toml_path.display()
				)
				.into())
			}
		};

		let old_version = old_version_str
			.as_str()
			.unwrap()
			.parse::<Version>()
			.map_err(|e| format!("the crate version field is invalid: {}", e))?;
		let new_version = match cmd {
			Command::Get => {
				println!("{}", &old_version);
				return Ok(());
			}
			Command::Set(new_version) => new_version,
			Command::Bump(field) => {
				let mut new_version = old_version.clone();
				match field {
					VersionField::Patch => new_version.bump_patch(),
					VersionField::Minor => new_version.bump_minor(),
					VersionField::Major => new_version.bump_major(),
				};
				new_version
			}
		};

		if new_version.eq(&old_version) {
			println!(
				"the new version is the same as the old one ({}); no action taken",
				&old_version
			);
			return Ok(());
		}
		let val = toml_edit::value(new_version.to_string());
		let decor = old_version_str.decor();
		let val = toml_edit::decorated(
			val.as_value().unwrap().clone(),
			decor.prefix(),
			decor.suffix(),
		);
		*old_version_str = val;

		let data = man.to_string();
		fs::write(&toml_path, data.as_bytes())?;
		println!("updated version: {} -> {}", &old_version, &new_version);
		Ok(())
	}
}

fn find_toml() -> Result<PathBuf, Box<dyn Error>> {
	let mut dir = env::current_dir()?;
	loop {
		dir.push("Cargo.toml");
		if dir.is_file() {
			return Ok(dir);
		}
		dir.pop();
		if !dir.pop() {
			return Err("could not locate a `Cargo.toml` in any of the current directory or the parent directories".into());
		}
	}
}