use serde::Serialize;
use crate::digest::Digest;
use crate::landing::Kind;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "op", rename_all = "kebab-case")]
pub enum Operation {
WriteFile {
path: String,
kind: Kind,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<Digest>,
after: Digest,
},
SpliceBlock {
path: String,
marker: String,
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<Digest>,
after: Digest,
},
RemoveOwnedFile {
path: String,
before: Digest,
},
WriteRecord {
#[serde(skip_serializing_if = "Option::is_none")]
before: Option<Digest>,
after: Digest,
},
UpdatePin {
manager: String,
before: String,
after: String,
},
}
impl Operation {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::WriteFile { .. } => "write-file",
Self::SpliceBlock { .. } => "splice-block",
Self::RemoveOwnedFile { .. } => "remove-owned-file",
Self::WriteRecord { .. } => "write-record",
Self::UpdatePin { .. } => "update-pin",
}
}
#[must_use]
pub fn path(&self) -> Option<&str> {
match self {
Self::WriteFile { path, .. }
| Self::SpliceBlock { path, .. }
| Self::RemoveOwnedFile { path, .. } => Some(path),
Self::WriteRecord { .. } | Self::UpdatePin { .. } => None,
}
}
#[must_use]
pub fn canonical(&self) -> String {
match self {
Self::WriteFile {
path,
kind,
before,
after,
} => format!(
"write-file\t{path}\t{}\t{}\t{after}",
kind.as_str(),
digest_or_absent(before.as_ref())
),
Self::SpliceBlock {
path,
marker,
before,
after,
} => format!(
"splice-block\t{path}\t{marker}\t{}\t{after}",
digest_or_absent(before.as_ref())
),
Self::RemoveOwnedFile { path, before } => {
format!("remove-owned-file\t{path}\t{before}")
}
Self::WriteRecord { before, after } => {
format!(
"write-record\t{}\t{after}",
digest_or_absent(before.as_ref())
)
}
Self::UpdatePin {
manager,
before,
after,
} => format!("update-pin\t{manager}\t{before}\t{after}"),
}
}
}
fn digest_or_absent(digest: Option<&Digest>) -> String {
digest.map_or_else(|| "absent".to_owned(), ToString::to_string)
}
#[cfg(test)]
mod tests {
use super::Operation;
use crate::digest::Digest;
use crate::landing::Kind;
#[test]
fn every_operation_serializes_under_its_tag() {
let a = Digest::of(b"a");
let b = Digest::of(b"b");
let cases = [
(
Operation::WriteFile {
path: "SECURITY.md".into(),
kind: Kind::Rendered,
before: None,
after: b.clone(),
},
format!(
r#"{{"op":"write-file","path":"SECURITY.md","kind":"rendered","after":"{b}"}}"#
),
),
(
Operation::SpliceBlock {
path: "AGENTS.md".into(),
marker: "<!-- BEGIN release-kit -->".into(),
before: Some(a.clone()),
after: b.clone(),
},
format!(
r#"{{"op":"splice-block","path":"AGENTS.md","marker":"<!-- BEGIN release-kit -->","before":"{a}","after":"{b}"}}"#
),
),
(
Operation::RemoveOwnedFile {
path: "old.yml".into(),
before: a.clone(),
},
format!(r#"{{"op":"remove-owned-file","path":"old.yml","before":"{a}"}}"#),
),
(
Operation::WriteRecord {
before: Some(a.clone()),
after: b.clone(),
},
format!(r#"{{"op":"write-record","before":"{a}","after":"{b}"}}"#),
),
(
Operation::UpdatePin {
manager: "mise".into(),
before: "0.1.0".into(),
after: "0.2.0".into(),
},
r#"{"op":"update-pin","manager":"mise","before":"0.1.0","after":"0.2.0"}"#
.to_owned(),
),
];
for (operation, expected) in cases {
assert_eq!(
serde_json::to_string(&operation).expect("an operation serializes"),
expected
);
assert!(operation.canonical().starts_with(operation.kind()));
}
}
}