use serde::{Deserialize, Serialize};
pub const READY_EVENT: &str = "ferrox.server.ready";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerReady {
pub event: String,
pub addr: String,
pub port: u16,
pub scheme: String,
pub pid: u32,
pub version: String,
}
impl ServerReady {
pub fn new(addr: std::net::SocketAddr, scheme: &str, version: &str, pid: u32) -> Self {
ServerReady {
event: READY_EVENT.to_string(),
addr: addr.to_string(),
port: addr.port(),
scheme: scheme.to_string(),
pid,
version: version.to_string(),
}
}
pub fn to_line(&self) -> String {
serde_json::to_string(self).expect("ServerReady is plain data and cannot fail to serialize")
}
pub fn from_line(line: &str) -> Option<Self> {
let parsed: ServerReady = serde_json::from_str(line.trim()).ok()?;
(parsed.event == READY_EVENT).then_some(parsed)
}
pub fn base_url(&self) -> String {
format!("{}://{}", self.scheme, self.addr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
fn ready(port: u16) -> ServerReady {
ServerReady::new(
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
"http",
"0.5.0",
4242,
)
}
#[test]
fn round_trips_through_one_stdout_line() {
let line = ready(51234).to_line();
assert!(!line.contains('\n'), "the ready line must be a single line");
assert_eq!(ServerReady::from_line(&line), Some(ready(51234)));
}
#[test]
fn ignores_lines_that_are_not_the_ready_event() {
assert!(ServerReady::from_line("2026-08-14 INFO listening").is_none());
assert!(ServerReady::from_line("{\"event\":\"something.else\"}").is_none());
assert!(ServerReady::from_line("").is_none());
}
#[test]
fn port_survives_an_ipv6_address_without_parsing_addr() {
let ready = ServerReady::new(
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8383),
"https",
"0.5.0",
7,
);
assert_eq!(ready.port, 8383);
assert_eq!(ready.base_url(), "https://[::1]:8383");
}
}