use serde::{Deserialize, Serialize};
pub const DAEMON_PROTOCOL_VERSION: u32 = 1;
pub const ARTIFACT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Handshake {
pub leindex_version: String,
pub daemon_protocol_version: u32,
pub artifact_format_version: u32,
pub worker_protocol_version: u32,
pub capabilities: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub enum HandshakeError {
ProtocolMismatch {
client: u32,
daemon: u32,
},
ArtifactMismatch {
client: u32,
daemon: u32,
},
}
impl HandshakeError {
pub fn actionable_message(&self) -> String {
match self {
HandshakeError::ProtocolMismatch { client, daemon } => {
format!(
"Client daemon protocol v{client} cannot talk to daemon v{daemon}. \
Restart leindexd: leindex cleanup --stale-daemons && leindexd"
)
}
HandshakeError::ArtifactMismatch { client, daemon } => {
format!(
"Client artifact format v{client} cannot read artifacts from daemon v{daemon}. \
Re-index the project: leindex index --force"
)
}
}
}
}
impl Handshake {
pub fn current() -> Self {
Handshake {
leindex_version: env!("CARGO_PKG_VERSION").to_string(),
daemon_protocol_version: DAEMON_PROTOCOL_VERSION,
artifact_format_version: ARTIFACT_FORMAT_VERSION,
worker_protocol_version: 1,
capabilities: Vec::new(),
}
}
pub fn validate_against(&self, other: &Handshake) -> Result<(), HandshakeError> {
if self.daemon_protocol_version != other.daemon_protocol_version {
return Err(HandshakeError::ProtocolMismatch {
client: self.daemon_protocol_version,
daemon: other.daemon_protocol_version,
});
}
if self.artifact_format_version != other.artifact_format_version {
return Err(HandshakeError::ArtifactMismatch {
client: self.artifact_format_version,
daemon: other.artifact_format_version,
});
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_matching_handshake_ok() {
let h = Handshake::current();
assert!(h.validate_against(&h).is_ok());
}
#[test]
fn test_protocol_version_mismatch_rejected() {
let a = Handshake::current();
let mut b = a.clone();
b.daemon_protocol_version = a.daemon_protocol_version + 1;
assert_eq!(
a.validate_against(&b),
Err(HandshakeError::ProtocolMismatch {
client: a.daemon_protocol_version,
daemon: b.daemon_protocol_version,
}),
);
}
#[test]
fn test_artifact_version_mismatch_rejected() {
let a = Handshake::current();
let mut b = a.clone();
b.artifact_format_version = a.artifact_format_version + 1;
assert_eq!(
a.validate_against(&b),
Err(HandshakeError::ArtifactMismatch {
client: a.artifact_format_version,
daemon: b.artifact_format_version,
}),
);
}
#[test]
fn test_current_reads_constants() {
let h = Handshake::current();
assert_eq!(h.daemon_protocol_version, DAEMON_PROTOCOL_VERSION);
assert_eq!(h.artifact_format_version, ARTIFACT_FORMAT_VERSION);
assert!(!h.leindex_version.is_empty());
}
#[test]
fn test_validate_labels_correctly() {
let client = Handshake {
leindex_version: "1.0.0".into(),
daemon_protocol_version: 1,
artifact_format_version: 1,
worker_protocol_version: 1,
capabilities: vec![],
};
let daemon = Handshake {
leindex_version: "1.0.0".into(),
daemon_protocol_version: 2,
artifact_format_version: 1,
worker_protocol_version: 1,
capabilities: vec![],
};
assert_eq!(
client.validate_against(&daemon),
Err(HandshakeError::ProtocolMismatch {
client: 1,
daemon: 2,
}),
);
assert_eq!(
daemon.validate_against(&client),
Err(HandshakeError::ProtocolMismatch {
client: 2,
daemon: 1,
}),
);
}
#[test]
fn test_protocol_mismatch_actionable_message() {
let err = HandshakeError::ProtocolMismatch {
client: 1,
daemon: 2,
};
let msg = err.actionable_message();
assert!(
msg.contains("v1"),
"actionable message must contain client version: {msg}"
);
assert!(
msg.contains("v2"),
"actionable message must contain daemon version: {msg}"
);
assert!(
msg.contains("leindex cleanup --stale-daemons"),
"actionable message must contain restart instructions: {msg}"
);
assert!(
msg.contains("leindexd"),
"actionable message must mention restarting leindexd: {msg}"
);
}
#[test]
fn test_artifact_mismatch_actionable_message() {
let err = HandshakeError::ArtifactMismatch {
client: 1,
daemon: 2,
};
let msg = err.actionable_message();
assert!(
msg.contains("v1"),
"message must contain client version: {msg}"
);
assert!(
msg.contains("v2"),
"message must contain daemon version: {msg}"
);
assert!(
msg.contains("leindex index --force"),
"message must contain re-index instructions: {msg}"
);
}
}