use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProtocolVersion {
pub semantic: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_describe: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_hash: Option<String>,
}
pub const WIRE_REVISION: u32 = 6;
impl ProtocolVersion {
pub fn current() -> Self {
Self {
semantic: format!("{}+w{}", env!("CARGO_PKG_VERSION"), WIRE_REVISION),
git_describe: option_env!("RCP_GIT_DESCRIBE").map(String::from),
git_hash: option_env!("RCP_GIT_HASH").map(String::from),
}
}
pub fn crate_version(&self) -> &str {
self.semantic.split('+').next().unwrap_or(&self.semantic)
}
pub fn is_compatible_with(&self, other: &Self) -> bool {
self.semantic == other.semantic
}
pub fn cache_tag(&self) -> String {
self.semantic.replace('+', "-")
}
pub fn display(&self) -> String {
if let Some(ref git_describe) = self.git_describe {
format!("{} ({})", self.semantic, git_describe)
} else {
self.semantic.clone()
}
}
pub fn to_json(&self) -> anyhow::Result<String> {
serde_json::to_string(self)
.map_err(|e| anyhow::anyhow!("failed to serialize version: {:#}", e))
}
pub fn from_json(json: &str) -> anyhow::Result<Self> {
serde_json::from_str(json)
.map_err(|e| anyhow::anyhow!("failed to parse version JSON: {:#}", e))
}
}
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg_attr(rcp_nix_sandbox, ignore = "Nix source build has no Git metadata")]
#[test]
fn test_current_version() {
let version = ProtocolVersion::current();
assert!(!version.semantic.is_empty());
assert!(
version.git_describe.is_some(),
"git_describe should be populated by build.rs (check that build.rs is activated in Cargo.toml)"
);
assert!(
version.git_hash.is_some(),
"git_hash should be populated by build.rs (check that build.rs is activated in Cargo.toml)"
);
}
#[test]
fn test_exact_version_compatibility() {
let v1 = ProtocolVersion {
semantic: "0.22.0".to_string(),
git_describe: None,
git_hash: None,
};
let v2 = ProtocolVersion {
semantic: "0.22.0".to_string(),
git_describe: Some("v0.21.1-7-g644da27".to_string()),
git_hash: Some("644da27".to_string()),
};
let v3 = ProtocolVersion {
semantic: "0.21.0".to_string(),
git_describe: None,
git_hash: None,
};
assert!(v1.is_compatible_with(&v2));
assert!(v2.is_compatible_with(&v1));
assert!(!v1.is_compatible_with(&v3));
assert!(!v3.is_compatible_with(&v1));
}
#[test]
fn test_display() {
let v1 = ProtocolVersion {
semantic: "0.22.0".to_string(),
git_describe: None,
git_hash: None,
};
assert_eq!(v1.display(), "0.22.0");
let v2 = ProtocolVersion {
semantic: "0.22.0".to_string(),
git_describe: Some("v0.21.1-7-g644da27".to_string()),
git_hash: None,
};
assert_eq!(v2.display(), "0.22.0 (v0.21.1-7-g644da27)");
}
#[test]
fn wire_revision_mismatch_is_incompatible_in_both_directions() {
let a = ProtocolVersion::current();
assert!(a.is_compatible_with(&a.clone()));
assert!(
a.semantic.contains("+w"),
"the compatibility revision must ride inside `semantic`, where even a pre-revision peer's string comparison sees it"
);
let mut b = a.clone();
b.semantic = format!("{}+w{}", a.crate_version(), WIRE_REVISION + 1);
assert!(
!a.is_compatible_with(&b),
"same release with a different remote protocol/process contract must be rejected"
);
let legacy = ProtocolVersion {
semantic: a.crate_version().to_string(),
git_describe: None,
git_hash: None,
};
assert!(!a.is_compatible_with(&legacy));
assert!(!legacy.is_compatible_with(&a));
assert_eq!(a.cache_tag(), a.semantic.replace('+', "-"));
}
#[test]
fn current_process_contract_rejects_revision_five_daemons() {
let current = ProtocolVersion::current();
let revision_five = ProtocolVersion {
semantic: format!("{}+w5", current.crate_version()),
git_describe: None,
git_hash: None,
};
assert!(!current.is_compatible_with(&revision_five));
assert!(!revision_five.is_compatible_with(¤t));
assert_eq!(
current.cache_tag(),
format!("{}-w6", current.crate_version())
);
assert_ne!(current.cache_tag(), revision_five.cache_tag());
}
#[test]
fn test_json_serialization() {
let v = ProtocolVersion {
semantic: "0.22.0".to_string(),
git_describe: Some("v0.21.1-7-g644da27".to_string()),
git_hash: Some("644da27abc".to_string()),
};
let json = v.to_json().unwrap();
let parsed = ProtocolVersion::from_json(&json).unwrap();
assert_eq!(v, parsed);
}
#[test]
fn test_json_deserialization_without_git() {
let json = r#"{"semantic":"0.22.0"}"#;
let v = ProtocolVersion::from_json(json).unwrap();
assert_eq!(v.semantic, "0.22.0");
assert!(v.git_describe.is_none());
assert!(v.git_hash.is_none());
}
}