use std::io::Write;
use rust_releases::{semver, Release, ReleaseIndex};
use toml_edit::{table, value, DocumentMut, Item, Value};
use crate::context::SetContext;
use crate::error::{InvalidMsrvSetError, IoError, IoErrorSource, SetMsrvError};
use crate::manifest::bare_version::BareVersion;
use crate::manifest::{CargoManifestParser, TomlParser};
use crate::reporter::event::{
AuxiliaryOutput, AuxiliaryOutputItem, Destination, MsrvKind, SetResult,
UnableToConfirmValidReleaseVersion,
};
use crate::reporter::Reporter;
use crate::{CargoMSRVError, SubCommand, TResult};
const RUST_VERSION_SUPPORTED_SINCE: semver::Version = semver::Version::new(1, 56, 0);
pub struct Set<'index> {
release_index: Option<&'index ReleaseIndex>,
}
impl<'index> Set<'index> {
pub fn new(release_index: Option<&'index ReleaseIndex>) -> Self {
Self { release_index }
}
}
impl SubCommand for Set<'_> {
type Context = SetContext;
type Output = ();
fn run(&self, ctx: &Self::Context, reporter: &impl Reporter) -> TResult<Self::Output> {
let configured_msrv = &ctx.msrv;
match self.release_index {
Some(index) if has_release(configured_msrv, index) => {
set_msrv(ctx, reporter, configured_msrv)
}
Some(index) => Err(InvalidMsrvSetError {
input: configured_msrv.clone(),
search_space: index.releases().to_vec(),
}
.into()),
None => {
reporter.report_event(UnableToConfirmValidReleaseVersion {})?;
set_msrv(ctx, reporter, configured_msrv)
}
}
}
}
fn has_release(configured_msrv: &BareVersion, release_index: &ReleaseIndex) -> bool {
release_index
.releases()
.iter()
.any(|release| matches_release(configured_msrv, release))
}
fn matches_release(msrv: &BareVersion, release: &Release) -> bool {
let major_match = release.version().major == msrv.major();
let minor_match = release.version().minor == msrv.minor();
let patch_match = msrv
.patch()
.is_none_or(|patch| release.version().patch == patch);
major_match && minor_match && patch_match
}
fn set_msrv(ctx: &SetContext, reporter: &impl Reporter, msrv: &BareVersion) -> TResult<()> {
let cargo_toml = ctx.environment.manifest();
let contents = std::fs::read_to_string(&cargo_toml).map_err(|error| IoError {
error,
source: IoErrorSource::ReadFile(cargo_toml.clone()),
})?;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(&contents)?;
check_workspace(&manifest)?;
set_or_override_msrv(&mut manifest, msrv)?;
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&cargo_toml)
.map_err(|error| IoError {
error,
source: IoErrorSource::OpenFile(cargo_toml.clone()),
})?;
write!(&mut file, "{}", manifest).map_err(|error| IoError {
error,
source: IoErrorSource::WriteFile(cargo_toml.to_path_buf()),
})?;
reporter.report_event(AuxiliaryOutput::new(
Destination::file(cargo_toml.clone()),
AuxiliaryOutputItem::msrv(MsrvKind::RustVersion),
))?;
reporter.report_event(SetResult::new(msrv.clone(), cargo_toml))?;
Ok(())
}
fn check_workspace(manifest: &DocumentMut) -> TResult<()> {
if manifest.as_table().get("package").is_none()
&& manifest.as_table().get("workspace").is_some()
{
Err(CargoMSRVError::WorkspaceFound)
} else {
Ok(())
}
}
fn set_or_override_msrv(manifest: &mut DocumentMut, msrv: &BareVersion) -> TResult<()> {
discard_current_msrv(manifest);
insert_new_msrv(manifest, msrv)
}
fn insert_new_msrv(manifest: &mut DocumentMut, msrv: &BareVersion) -> TResult<()> {
fn insert_rust_version(manifest: &mut DocumentMut, msrv: &BareVersion) -> TResult<()> {
manifest["package"]["rust-version"] = value(msrv.to_string());
Ok(())
}
fn insert_package_metadata_msrv(manifest: &mut DocumentMut, msrv: &BareVersion) -> TResult<()> {
let metadata_item = &mut manifest["package"]["metadata"];
match metadata_item {
Item::None => {
*metadata_item = table();
metadata_item["msrv"] = value(msrv.to_string());
}
Item::Value(Value::InlineTable(table)) => {
table.insert("msrv", msrv.to_string().into());
}
Item::Table(table) => {
table.insert("msrv", value(msrv.to_string()));
}
_ => return Err(CargoMSRVError::SetMsrv(SetMsrvError::NotATable)),
}
Ok(())
}
if msrv.to_semver_version() >= RUST_VERSION_SUPPORTED_SINCE {
insert_rust_version(manifest, msrv)
} else {
insert_package_metadata_msrv(manifest, msrv)
}
}
fn discard_current_msrv(document: &mut DocumentMut) {
fn get_package(document: &mut DocumentMut) -> Option<&mut Item> {
document.as_table_mut().get_mut("package")
}
fn get_metadata(document: &DocumentMut) -> Option<&Item> {
document
.as_table()
.get("package")
.and_then(|package| package.get("metadata"))
}
fn remove_rust_version(document: &mut DocumentMut) {
get_package(document)
.and_then(Item::as_table_like_mut)
.and_then(|package| package.remove("rust-version"));
}
fn remove_metadata_msrv(document: &mut DocumentMut) {
get_package(document)
.and_then(|package| package.get_mut("metadata"))
.and_then(Item::as_table_like_mut)
.and_then(|metadata| metadata.remove("msrv"));
if let Some(true) = get_metadata(document)
.and_then(Item::as_table_like)
.map(|metadata| metadata.is_empty())
{
get_package(document)
.and_then(Item::as_table_like_mut)
.map(|package| package.remove("metadata"));
}
}
remove_rust_version(document);
remove_metadata_msrv(document);
}
#[cfg(test)]
mod set_or_override_msrv_tests {
use toml_edit::DocumentMut;
use crate::manifest::bare_version::BareVersion;
use crate::manifest::{CargoManifestParser, TomlParser};
use crate::sub_command::set::set_or_override_msrv;
#[test]
fn set_rust_version_in_empty_two_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 56)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56"
);
}
#[test]
fn set_metadata_msrv_in_empty_two_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 10)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.10"
);
}
#[test]
fn set_rust_version_with_existing() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
rust-version = "1.58.0"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 56)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56"
);
}
#[test]
fn set_rust_version_with_existing_metadata_msrv() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.58.0"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 56)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56"
);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata");
assert!(metadata.is_none());
}
#[test]
fn set_rust_version_with_existing_metadata_msrv_and_other() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.58.0"
other = 1
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 56)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56"
);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata").unwrap();
let msrv = metadata.get("msrv");
assert!(msrv.is_none());
let other = metadata.get("other");
assert!(other.is_some());
}
#[test]
fn set_metadata_msrv_with_existing() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.11.0"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.11.0"
);
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 17)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.17"
);
}
#[test]
fn set_metadata_msrv_with_existing_rust_version() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
rust-version = "1.58"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.58"
);
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 17)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.17"
);
assert!(manifest
.get("package")
.and_then(|p| p.get("rust-version"))
.is_none());
}
#[test]
fn set_metadata_msrv_with_existing_and_other() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.58"
other = 1
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.58"
);
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 17)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.17"
);
assert_eq!(
manifest["package"]["metadata"]["other"]
.as_integer()
.unwrap(),
1
);
}
#[test]
fn set_rust_version_with_inline_table() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
metadata = { msrv = "1.15" }
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.15"
);
set_or_override_msrv(&mut manifest, &BareVersion::TwoComponents(1, 57)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.57"
);
assert!(manifest
.get("package")
.and_then(|p| p.get("metadata"))
.is_none());
}
}
#[cfg(test)]
mod discard_current_msrv_tests {
use toml_edit::DocumentMut;
use crate::manifest::{CargoManifestParser, TomlParser};
use crate::sub_command::set::discard_current_msrv;
#[test]
fn discard_none() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
let expected = manifest.clone();
discard_current_msrv(&mut manifest);
assert_eq!(manifest.to_string(), expected.to_string());
}
#[test]
fn discard_rust_version() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
rust-version = "1.56.0"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56.0"
);
discard_current_msrv(&mut manifest);
let package = manifest.get("package").unwrap();
let rust_version = package.get("rust-version");
assert!(rust_version.is_none());
}
#[test]
fn discard_package_metadata_msrv_with_empty_metadata() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.56.0"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.56.0"
);
discard_current_msrv(&mut manifest);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata");
assert!(metadata.is_none());
}
#[test]
fn discard_package_metadata_msrv_with_other_metadata_item() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.56.0"
other = 7
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.56.0"
);
discard_current_msrv(&mut manifest);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata").unwrap();
let msrv = metadata.get("msrv");
assert!(msrv.is_none());
assert_eq!(
manifest["package"]["metadata"]["other"]
.as_integer()
.unwrap(),
7,
);
}
#[test]
fn discard_package_metadata_inline_table() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
metadata = { msrv = "1.15" }
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.15"
);
discard_current_msrv(&mut manifest);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata");
assert!(metadata.is_none());
}
#[test]
fn discard_package_metadata_inline_table_and_other() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
metadata = { msrv = "1.15", other = 1 }
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.15"
);
discard_current_msrv(&mut manifest);
let package = manifest.get("package").unwrap();
let metadata = package.get("metadata").unwrap();
let msrv = metadata.get("msrv");
assert!(msrv.is_none());
let other = metadata.get("other");
assert!(other.is_some());
}
}
#[cfg(test)]
mod insert_new_msrv_tests {
use toml_edit::{DocumentMut, Item};
use crate::manifest::bare_version::BareVersion;
use crate::manifest::{CargoManifestParser, TomlParser};
use crate::sub_command::set::insert_new_msrv;
#[test]
fn insert_rust_version_in_empty_two_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &BareVersion::TwoComponents(1, 56)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56"
);
}
#[test]
fn insert_rust_version_in_empty_three_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &BareVersion::ThreeComponents(1, 56, 1)).unwrap();
assert_eq!(
manifest["package"]["rust-version"].as_str().unwrap(),
"1.56.1"
);
}
#[test]
fn insert_metadata_msrv_in_empty_two_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &BareVersion::TwoComponents(1, 10)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.10"
);
}
#[test]
fn insert_metadata_msrv_in_empty_three_component() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &BareVersion::ThreeComponents(1, 10, 1)).unwrap();
assert_eq!(
manifest["package"]["metadata"]["msrv"].as_str().unwrap(),
"1.10.1"
);
}
mod insert_package_manifest_msrv_correct_table_type {
use toml_edit::{DocumentMut, Item, Value};
use crate::manifest::bare_version::BareVersion;
use crate::manifest::{CargoManifestParser, TomlParser};
use crate::sub_command::set::insert_new_msrv;
const METADATA_MSRV: BareVersion = BareVersion::TwoComponents(1, 55);
#[test]
fn insert_without_preexisting_table() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let metadata = &manifest["package"]["metadata"];
assert!(matches!(metadata, Item::Table(_)));
let msrv = &metadata["msrv"];
assert!(matches!(msrv, Item::Value(Value::String(s)) if s.value() == "1.55"))
}
#[test]
fn insert_with_preexisting_rust_version() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
rust-version = "1.56"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let metadata = &manifest["package"]["metadata"];
assert!(matches!(metadata, Item::Table(_)));
let msrv = &metadata["msrv"];
assert!(matches!(msrv, Item::Value(Value::String(s)) if s.value() == "1.55"))
}
#[test]
fn insert_with_preexisting_package_metadata_msrv_regular_table() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[package.metadata]
msrv = "1.54"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let metadata = &manifest["package"]["metadata"];
assert!(matches!(metadata, Item::Table(_)));
let msrv = &metadata["msrv"];
assert!(matches!(msrv, Item::Value(Value::String(s)) if s.value() == "1.55"))
}
#[test]
fn insert_with_preexisting_package_metadata_msrv_inline_table() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
metadata = { msrv = "1.54" }
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let metadata = &manifest["package"]["metadata"];
assert!(matches!(metadata, Item::Value(Value::InlineTable(_))));
let msrv = &metadata["msrv"];
assert!(matches!(msrv, Item::Value(Value::String(s)) if s.value() == "1.55"))
}
#[test]
fn insert_with_preexisting_package_metadata_but_no_msrv() {
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
metadata = { k = "1.54" }
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let metadata = &manifest["package"]["metadata"];
assert!(matches!(metadata, Item::Value(Value::InlineTable(_))));
let msrv = &metadata["msrv"];
assert!(matches!(msrv, Item::Value(Value::String(s)) if s.value() == "1.55"));
let k = &metadata["k"];
assert!(matches!(k, Item::Value(Value::String(s)) if s.value() == "1.54"));
}
}
#[test]
fn set_and_reparse() {
const METADATA_MSRV: BareVersion = BareVersion::TwoComponents(1, 55);
let input = r#"[package]
name = "package_name"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
let mut manifest = CargoManifestParser.parse::<DocumentMut>(input).unwrap();
insert_new_msrv(&mut manifest, &METADATA_MSRV).unwrap();
let output = manifest.to_string();
let document = CargoManifestParser.parse::<DocumentMut>(&output).unwrap();
let msrv = document
.as_table()
.get("package")
.and_then(Item::as_table)
.and_then(|package| package.get("metadata"))
.and_then(Item::as_table_like)
.and_then(|metadata| metadata.get("msrv"))
.and_then(Item::as_str)
.unwrap();
assert_eq!(msrv, &METADATA_MSRV.to_string());
}
}
#[cfg(test)]
mod valid_release_tests {
use std::iter::FromIterator;
use cargo_metadata::semver;
use rust_releases::{Release, ReleaseIndex};
use crate::manifest::bare_version::BareVersion;
use crate::sub_command::set::has_release;
#[test]
fn releases_include_bare() {
let bare = BareVersion::TwoComponents(1, 55);
let index =
ReleaseIndex::from_iter(vec![Release::new_stable(semver::Version::new(1, 55, 0))]);
assert!(has_release(&bare, &index))
}
#[test]
fn releases_does_not_include_bare() {
let bare = BareVersion::TwoComponents(1, 55);
let index =
ReleaseIndex::from_iter(vec![Release::new_stable(semver::Version::new(1, 54, 0))]);
assert!(!has_release(&bare, &index))
}
#[test]
fn releases_includes_bare_with_patch() {
let bare = BareVersion::ThreeComponents(1, 55, 100);
let index =
ReleaseIndex::from_iter(vec![Release::new_stable(semver::Version::new(1, 55, 100))]);
assert!(has_release(&bare, &index))
}
#[test]
fn releases_does_not_include_bare_with_patch() {
let bare = BareVersion::ThreeComponents(1, 55, 100);
let index =
ReleaseIndex::from_iter(vec![Release::new_stable(semver::Version::new(1, 55, 0))]);
assert!(!has_release(&bare, &index))
}
}