use serde::{Deserialize, Serialize};
pub const DISCOVERY_PORT: u16 = 53535;
pub const DISCOVER_REQ: &str = "AI_DISCOVER_REQ";
pub const DISCOVER_RES: &str = "AI_DISCOVER_RES";
pub const SERVICE_ANNOUNCE: &str = "AI_SERVICE_ANNOUNCE";
pub const SERVICE_GOODBYE: &str = "AI_SERVICE_GOODBYE";
pub const PROTOCOL_VERSION: &str = "1.0";
pub const BROADCAST_ADDR: &str = "255.255.255.255";
pub fn parse_message(
data: &[u8],
) -> std::result::Result<(String, serde_json::Value), ProtocolError> {
let text = String::from_utf8(data.to_vec())
.map_err(|e| ProtocolError::InvalidEncoding(e.to_string()))?;
let text = text.trim();
let lines: Vec<&str> = text.splitn(2, '\n').collect();
if lines.len() != 2 {
return Err(ProtocolError::InvalidFormat(format!(
"Expected COMMAND\\nJSON, got: {}",
&text[..text.len().min(100)]
)));
}
let cmd = lines[0].trim().to_string();
let payload: serde_json::Value =
serde_json::from_str(lines[1]).map_err(|e| ProtocolError::InvalidJson(e.to_string()))?;
Ok((cmd, payload))
}
pub fn build_discover_req(query_id: Option<&str>) -> Vec<u8> {
let query_id = query_id
.map(String::from)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let payload = serde_json::json!({
"query_id": query_id,
"version": PROTOCOL_VERSION,
});
format!(
"{}\n{}",
DISCOVER_REQ,
serde_json::to_string(&payload).unwrap()
)
.into_bytes()
}
#[derive(Debug)]
pub struct DiscoverResParams<'a> {
pub query_id: &'a str,
pub port: u16,
pub manifest_data: &'a serde_json::Value,
}
pub fn build_discover_res(params: DiscoverResParams) -> Vec<u8> {
let payload = serde_json::json!({
"query_id": params.query_id,
"port": params.port,
"manifest": params.manifest_data,
});
format!(
"{}\n{}",
DISCOVER_RES,
serde_json::to_string(&payload).unwrap()
)
.into_bytes()
}
pub fn build_announce(http_port: u16, manifest_data: &serde_json::Value) -> Vec<u8> {
let payload = serde_json::json!({
"event": "online",
"port": http_port,
"manifest": manifest_data,
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
});
format!(
"{}\n{}",
SERVICE_ANNOUNCE,
serde_json::to_string(&payload).unwrap()
)
.into_bytes()
}
pub fn build_goodbye(service_id: &str, service_name: &str) -> Vec<u8> {
let payload = serde_json::json!({
"event": "offline",
"service_id": service_id,
"service_name": service_name,
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
"version": PROTOCOL_VERSION,
});
format!(
"{}\n{}",
SERVICE_GOODBYE,
serde_json::to_string(&payload).unwrap()
)
.into_bytes()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ServiceInfo {
pub query_id: String,
pub port: u16,
pub ip: String,
pub manifest: serde_json::Value,
}
impl ServiceInfo {
pub fn base_url(&self) -> String {
format!("http://{}:{}", self.ip, self.port)
}
pub fn from_payload(payload: &serde_json::Value, ip: &str) -> Self {
Self {
query_id: payload
.get("query_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
port: payload.get("port").and_then(|v| v.as_u64()).unwrap_or(80) as u16,
ip: ip.to_string(),
manifest: payload
.get("manifest")
.cloned()
.unwrap_or(serde_json::Value::Null),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ServiceEvent {
pub event: String,
pub port: u16,
pub ip: String,
pub manifest: serde_json::Value,
pub timestamp: u64,
}
impl ServiceEvent {
pub fn base_url(&self) -> String {
format!("http://{}:{}", self.ip, self.port)
}
pub fn from_payload(payload: &serde_json::Value, ip: &str) -> Self {
Self {
event: payload
.get("event")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
port: payload.get("port").and_then(|v| v.as_u64()).unwrap_or(80) as u16,
ip: ip.to_string(),
manifest: payload
.get("manifest")
.cloned()
.unwrap_or(serde_json::Value::Null),
timestamp: payload
.get("timestamp")
.and_then(|v| v.as_u64())
.unwrap_or(0),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("Invalid encoding: {0}")]
InvalidEncoding(String),
#[error("Invalid format: {0}")]
InvalidFormat(String),
#[error("Invalid JSON: {0}")]
InvalidJson(String),
}