use std::collections::BTreeMap;
use std::fmt;
use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::ConfigValueSource;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PatchSourceKind {
Path,
}
impl PatchSourceKind {
pub const fn as_key(self) -> &'static str {
match self {
PatchSourceKind::Path => "path",
}
}
}
impl fmt::Display for PatchSourceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_key())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum PatchSource {
Path { path: Utf8PathBuf },
}
impl PatchSource {
pub fn kind(&self) -> PatchSourceKind {
match self {
PatchSource::Path { .. } => PatchSourceKind::Path,
}
}
pub fn from_path_field(
package: &str,
raw_path: Option<String>,
) -> Result<PatchSource, PatchValidationError> {
match raw_path {
Some(path) if !path.trim().is_empty() => Ok(PatchSource::Path {
path: Utf8PathBuf::from(path.trim()),
}),
_ => Err(PatchValidationError::MissingSource {
package: package.to_owned(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PatchProvenance {
Manifest,
Config(ConfigValueSource),
}
impl PatchProvenance {
pub fn as_key(self) -> String {
match self {
PatchProvenance::Manifest => "manifest".to_owned(),
PatchProvenance::Config(source) => source.as_key().to_owned(),
}
}
}
impl fmt::Display for PatchProvenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_key())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredPatch {
pub source: PatchSource,
pub declared_in: Utf8PathBuf,
pub provenance: PatchProvenance,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PatchManifestSettings {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub entries: BTreeMap<crate::PackageName, PatchSource>,
}
impl PatchManifestSettings {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PatchValidationError {
#[error("patch for package `{package}` is missing a source; expected `path = \"...\"`")]
MissingSource { package: String },
#[error(
"patch for package `{package}` points to `{path}`, but that path does not contain a cabin.toml"
)]
MissingManifest { package: String, path: String },
#[error(
"patch for package `{package}` points to `{path}`, but its cabin.toml declares no `[package]`"
)]
ManifestHasNoPackage { package: String, path: String },
#[error(
"patch for package `{package}` points to package `{actual}`; patch package name must match `{package}`"
)]
PackageNameMismatch { package: String, actual: String },
#[error(
"patch package `{package}` has version `{version}`, which does not satisfy dependency requirement `{requirement}`"
)]
VersionMismatch {
package: String,
version: String,
requirement: String,
},
#[error(
"multiple patches for package `{package}` are active at the same precedence level; remove one patch declaration"
)]
DuplicateAtSameLevel { package: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_path_field_accepts_and_trims_paths() {
for (raw, expected) in [
("../fmt", "../fmt"),
("./local", "./local"),
(" ../fmt \n", "../fmt"),
] {
let source = PatchSource::from_path_field("fmt", Some(raw.to_owned())).unwrap();
assert_eq!(
source,
PatchSource::Path {
path: Utf8PathBuf::from(expected),
},
);
}
}
#[test]
fn from_path_field_rejects_missing_and_blank_paths() {
for raw in [None, Some(String::new()), Some(" ".to_owned())] {
let err = PatchSource::from_path_field("fmt", raw).unwrap_err();
assert_eq!(
err,
PatchValidationError::MissingSource {
package: "fmt".to_owned(),
},
);
}
}
#[test]
fn source_kind_key_and_display_are_stable() {
let source = PatchSource::Path {
path: Utf8PathBuf::from("../fmt"),
};
assert_eq!(source.kind(), PatchSourceKind::Path);
assert_eq!(PatchSourceKind::Path.as_key(), "path");
assert_eq!(PatchSourceKind::Path.to_string(), "path");
}
#[test]
fn provenance_keys_match_display() {
for (provenance, key) in [
(PatchProvenance::Manifest, "manifest"),
(
PatchProvenance::Config(ConfigValueSource::UserConfig),
"user-config",
),
(
PatchProvenance::Config(ConfigValueSource::WorkspaceConfig),
"workspace-config",
),
] {
assert_eq!(provenance.as_key(), key);
assert_eq!(provenance.to_string(), key);
}
}
#[test]
fn manifest_settings_is_empty_tracks_entries() {
let mut settings = PatchManifestSettings::default();
assert!(settings.is_empty());
settings.entries.insert(
crate::PackageName::new("fmt").unwrap(),
PatchSource::Path {
path: Utf8PathBuf::from("../fmt"),
},
);
assert!(!settings.is_empty());
}
}