use crate::release::publish::tag::{is_crate_tag, is_platform_tag, is_transaction_tag};
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TagKind {
CrateVersion,
Platform,
Transaction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TagPolicyError {
pub(crate) tag: String,
pub(crate) reason: String,
}
impl std::fmt::Display for TagPolicyError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"tag {:?} does not conform to any release tag policy: {}",
self.tag, self.reason
)
}
}
impl std::error::Error for TagPolicyError {}
#[allow(dead_code)]
pub(crate) fn classify_tag(tag: &str) -> Result<TagKind, TagPolicyError> {
if is_crate_tag(tag) {
Ok(TagKind::CrateVersion)
} else if is_platform_tag(tag) {
Ok(TagKind::Platform)
} else if is_transaction_tag(tag) {
Ok(TagKind::Transaction)
} else {
Err(TagPolicyError {
tag: tag.to_string(),
reason: "expected one of: crate-version (<crate>-v<YBF>), \
platform (v<YEAR.BREAK>), or transaction (release-*)"
.to_string(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TriggerAction {
Accept,
Reject,
}
#[allow(dead_code)]
pub(crate) fn transaction_trigger_action(kind: TagKind) -> TriggerAction {
match kind {
TagKind::Transaction => TriggerAction::Accept,
TagKind::Platform | TagKind::CrateVersion => TriggerAction::Reject,
}
}
#[allow(dead_code)]
pub(crate) fn platform_trigger_action(kind: TagKind) -> TriggerAction {
match kind {
TagKind::Platform => TriggerAction::Accept,
TagKind::Transaction | TagKind::CrateVersion => TriggerAction::Reject,
}
}
#[allow(dead_code)]
pub(crate) fn allows_prepublish_tagging(_kind: TagKind) -> bool {
false
}
#[allow(dead_code)]
pub(crate) fn verify_crate_tag(
tag: &str,
expected_crate: &str,
expected_version: &crate::release::version::Ybf,
) -> Result<(), TagPolicyError> {
let parsed =
crate::release::publish::tag::parse_crate_tag(tag).ok_or_else(|| TagPolicyError {
tag: tag.to_string(),
reason: "not a valid crate version tag".to_string(),
})?;
if parsed.crate_name != expected_crate {
return Err(TagPolicyError {
tag: tag.to_string(),
reason: format!(
"crate name mismatch: expected {expected_crate}, found {}",
parsed.crate_name
),
});
}
if parsed.version != *expected_version {
return Err(TagPolicyError {
tag: tag.to_string(),
reason: format!(
"version mismatch: expected {expected_version}, found {}",
parsed.version
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::release::version::Ybf;
#[test]
fn classify_crate_version_tag() {
assert_eq!(
classify_tag("arcature-auth-v2026.1.7").unwrap(),
TagKind::CrateVersion
);
assert_eq!(
classify_tag("arcature-dx-v2026.2.0").unwrap(),
TagKind::CrateVersion
);
}
#[test]
fn classify_platform_tag() {
assert_eq!(classify_tag("v2026.1").unwrap(), TagKind::Platform);
assert_eq!(classify_tag("v2027.0").unwrap(), TagKind::Platform);
}
#[test]
fn classify_transaction_tag() {
assert_eq!(
classify_tag("release-2026-08-15.01").unwrap(),
TagKind::Transaction
);
assert_eq!(
classify_tag("release-2026-12-31.99").unwrap(),
TagKind::Transaction
);
}
#[test]
fn classify_unknown_tag_errors() {
assert!(classify_tag("v2026.1.0").is_err());
assert!(classify_tag("arcature-auth-2026.1.7").is_err());
assert!(classify_tag("latest").is_err());
assert!(classify_tag("").is_err());
}
#[test]
fn transaction_accepts_only_transaction_tags() {
assert_eq!(
transaction_trigger_action(TagKind::Transaction),
TriggerAction::Accept
);
assert_eq!(
transaction_trigger_action(TagKind::Platform),
TriggerAction::Reject
);
assert_eq!(
transaction_trigger_action(TagKind::CrateVersion),
TriggerAction::Reject
);
}
#[test]
fn platform_accepts_only_platform_tags() {
assert_eq!(
platform_trigger_action(TagKind::Platform),
TriggerAction::Accept
);
assert_eq!(
platform_trigger_action(TagKind::Transaction),
TriggerAction::Reject
);
assert_eq!(
platform_trigger_action(TagKind::CrateVersion),
TriggerAction::Reject
);
}
#[test]
fn no_prepublish_tagging_for_any_kind() {
assert!(!allows_prepublish_tagging(TagKind::CrateVersion));
assert!(!allows_prepublish_tagging(TagKind::Platform));
assert!(!allows_prepublish_tagging(TagKind::Transaction));
}
#[test]
fn verify_matching_crate_tag() {
let version = Ybf::parse("2026.1.7").unwrap();
assert!(verify_crate_tag("arcature-auth-v2026.1.7", "arcature-auth", &version).is_ok());
}
#[test]
fn verify_crate_tag_name_mismatch() {
let version = Ybf::parse("2026.1.7").unwrap();
let error = verify_crate_tag("arcature-cli-v2026.1.7", "arcature-auth", &version)
.expect_err("name mismatch should fail");
assert!(error.reason.contains("crate name mismatch"));
}
#[test]
fn verify_crate_tag_version_mismatch() {
let version = Ybf::parse("2026.1.7").unwrap();
let error = verify_crate_tag("arcature-auth-v2026.1.8", "arcature-auth", &version)
.expect_err("version mismatch should fail");
assert!(error.reason.contains("version mismatch"));
}
#[test]
fn verify_invalid_crate_tag() {
let version = Ybf::parse("2026.1.7").unwrap();
assert!(verify_crate_tag("not-a-tag", "arcature-auth", &version).is_err());
}
#[test]
fn three_tag_kinds_are_mutually_exclusive() {
let crate_tags = ["arcature-auth-v2026.1.7", "arcature-dx-v2026.2.0"];
let platform_tags = ["v2026.1", "v2027.0"];
let transaction_tags = ["release-2026-08-15.01", "release-2026-12-31.99"];
for tag in crate_tags {
assert_eq!(classify_tag(tag).unwrap(), TagKind::CrateVersion);
assert_ne!(classify_tag(tag).unwrap(), TagKind::Platform);
assert_ne!(classify_tag(tag).unwrap(), TagKind::Transaction);
}
for tag in platform_tags {
assert_eq!(classify_tag(tag).unwrap(), TagKind::Platform);
assert_ne!(classify_tag(tag).unwrap(), TagKind::CrateVersion);
assert_ne!(classify_tag(tag).unwrap(), TagKind::Transaction);
}
for tag in transaction_tags {
assert_eq!(classify_tag(tag).unwrap(), TagKind::Transaction);
assert_ne!(classify_tag(tag).unwrap(), TagKind::CrateVersion);
assert_ne!(classify_tag(tag).unwrap(), TagKind::Platform);
}
}
}