use std::collections::BTreeMap;
use anyhow::Context;
use serde::Deserialize;
use crate::{LintLevel, OverrideMap, QueryOverride, RequiredSemverUpdate};
#[derive(Debug, Clone)]
pub(crate) struct Manifest {
pub(crate) path: std::path::PathBuf,
pub(crate) parsed: cargo_toml::Manifest<MetadataTable>,
}
impl Manifest {
pub(crate) fn parse(path: std::path::PathBuf) -> anyhow::Result<Self> {
let parsed = cargo_toml::Manifest::from_path_with_metadata(&path)
.with_context(|| format!("failed when reading {}", path.display()))?;
Ok(Self { path, parsed })
}
}
pub(crate) fn get_package_name(manifest: &Manifest) -> anyhow::Result<&str> {
let package = manifest.parsed.package.as_ref().with_context(|| {
format!(
"failed to parse {}: no `package` table",
manifest.path.display()
)
})?;
Ok(&package.name)
}
pub(crate) fn get_package_version(manifest: &Manifest) -> anyhow::Result<&str> {
let package = manifest.parsed.package.as_ref().with_context(|| {
format!(
"failed to parse {}: no `package` table",
manifest.path.display()
)
})?;
let version = package.version.get().with_context(|| {
format!(
"failed to retrieve package version from {}",
manifest.path.display()
)
})?;
Ok(version)
}
pub(crate) fn get_project_dir_from_manifest_path(
manifest_path: &std::path::Path,
) -> anyhow::Result<std::path::PathBuf> {
assert!(
manifest_path.ends_with("Cargo.toml"),
"path {} isn't pointing to a manifest",
manifest_path.display()
);
let dir_path = manifest_path
.parent()
.context("manifest path doesn't have a parent")?;
Ok(dir_path.to_path_buf())
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct MetadataTable {
#[serde(default, rename = "cargo-semver-checks")]
pub(crate) config: Option<SemverChecksTable>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub(crate) struct SemverChecksTable {
pub(crate) lints: Option<LintTable>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct LintTable {
#[serde(default, deserialize_with = "deserialize_workspace_key")]
pub(crate) workspace: bool,
#[serde(flatten, deserialize_with = "deserialize_into_overridemap")]
pub(crate) inner: OverrideMap,
}
impl From<LintTable> for OverrideMap {
fn from(value: LintTable) -> OverrideMap {
value.inner
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub(crate) enum OverrideConfig {
#[serde(rename_all = "kebab-case")]
Structure {
#[serde(default)]
level: Option<LintLevel>,
#[serde(default)]
required_update: Option<RequiredSemverUpdate>,
},
LintLevel(LintLevel),
}
impl From<OverrideConfig> for QueryOverride {
fn from(value: OverrideConfig) -> Self {
match value {
OverrideConfig::Structure {
level,
required_update,
} => Self {
lint_level: level,
required_update,
},
OverrideConfig::LintLevel(lint_level) => Self {
lint_level: Some(lint_level),
required_update: None,
},
}
}
}
fn deserialize_into_overridemap<'de, D>(de: D) -> Result<OverrideMap, D::Error>
where
D: serde::de::Deserializer<'de>,
{
BTreeMap::<String, OverrideConfig>::deserialize(de)
.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect())
}
fn deserialize_workspace_key<'de, D>(de: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
let option = Option::<bool>::deserialize(de)?;
match option {
Some(true) => Ok(true),
None => Ok(false),
Some(false) => Err(serde::de::Error::custom("`lints.workspace = false` is not valid configuration. Either set `lints.workspace = true` or omit the key entirely.")),
}
}
pub(crate) fn deserialize_lint_table(
metadata: &serde_json::Value,
) -> anyhow::Result<Option<LintTable>> {
let table = Option::<MetadataTable>::deserialize(metadata)?;
Ok(table.and_then(|table| table.config.and_then(|config| config.lints)))
}
#[cfg(test)]
mod tests {
use super::{LintTable, MetadataTable};
use crate::QueryOverride;
#[test]
fn test_deserialize_config() {
use crate::LintLevel::*;
use crate::RequiredSemverUpdate::*;
let manifest = r#"[package]
name = "cargo-semver-checks"
version = "1.2.3"
edition = "2021"
[package.metadata.cargo-semver-checks.lints]
workspace = true
two = "deny"
three = { level = "warn" }
four = { required-update = "major" }
five = { required-update = "minor", level = "allow" }
[workspace.metadata.cargo-semver-checks.lints]
six = "allow"
"#;
let parsed = cargo_toml::Manifest::from_slice_with_metadata(manifest.as_bytes())
.expect("Cargo.toml should be valid");
let package_metadata: MetadataTable = parsed
.package
.expect("Cargo.toml should contain a package")
.metadata
.expect("Package metadata should be present");
let workspace_metadata = parsed
.workspace
.expect("Cargo.toml should contain a workspace")
.metadata
.expect("Workspace metadata should be present");
let pkg_table = package_metadata
.config
.expect("Semver checks table should be present")
.lints
.expect("Lint table should be present");
assert!(
pkg_table.workspace,
"Package lints table should contain `workspace = true`"
);
let pkg = pkg_table.inner;
let wks = workspace_metadata
.config
.expect("Semver checks table should be present")
.lints
.expect("Lint table should be present")
.inner;
assert!(
matches!(
pkg.get("two"),
Some(&QueryOverride {
lint_level: Some(Deny),
required_update: None,
})
),
"got {:?}",
pkg.get("two")
);
assert!(
matches!(
pkg.get("three"),
Some(&QueryOverride {
required_update: None,
lint_level: Some(Warn)
})
),
"got {:?}",
pkg.get("three")
);
assert!(
matches!(
pkg.get("four"),
Some(&QueryOverride {
required_update: Some(Major),
lint_level: None,
})
),
"got {:?}",
pkg.get("four")
);
assert!(
matches!(
pkg.get("five"),
Some(&QueryOverride {
required_update: Some(Minor),
lint_level: Some(Allow)
})
),
"got {:?}",
pkg.get("five")
);
assert!(
matches!(
wks.get("six"),
Some(&QueryOverride {
lint_level: Some(Allow),
required_update: None
})
),
"got {:?}",
wks.get("six")
);
}
#[test]
fn workspace_key_false_is_error() {
serde_json::from_value::<LintTable>(serde_json::json! {{
"workspace": false
}})
.expect_err("`workspace = false` should not be accepted");
}
#[test]
fn workspace_key_omitted_is_false() {
let table = serde_json::from_value::<LintTable>(serde_json::json! {{
}})
.expect("this should be a valid lint table");
assert!(!table.workspace, "table.workspace should be false");
}
}