use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Endpoint {
pub kind: String,
pub url: String,
#[serde(default)]
pub priority: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub health_hint_unix: Option<i64>,
}
impl Endpoint {
pub const KIND_CLOUDFLARE_QUICK: &'static str = "cloudflare_quick";
pub const KIND_CLOUDFLARE_NAMED: &'static str = "cloudflare_named";
pub const KIND_IROH: &'static str = "iroh";
pub const KIND_TAILSCALE_FUNNEL: &'static str = "tailscale_funnel";
pub const KIND_FRP: &'static str = "frp";
pub const KNOWN_KINDS: &'static [&'static str] = &[
Self::KIND_CLOUDFLARE_QUICK,
Self::KIND_CLOUDFLARE_NAMED,
Self::KIND_IROH,
Self::KIND_TAILSCALE_FUNNEL,
Self::KIND_FRP,
];
pub fn is_known_kind(&self) -> bool {
Self::KNOWN_KINDS.contains(&self.kind.as_str())
}
pub fn cloudflare_quick(url: impl Into<String>) -> Self {
Self {
kind: Self::KIND_CLOUDFLARE_QUICK.into(),
url: url.into(),
priority: 0,
health_hint_unix: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cloudflare_quick_builder() {
let e = Endpoint::cloudflare_quick("https://foo.trycloudflare.com");
assert_eq!(e.kind, "cloudflare_quick");
assert_eq!(e.url, "https://foo.trycloudflare.com");
assert_eq!(e.priority, 0);
assert!(e.is_known_kind());
}
#[test]
fn unknown_kind_preserved_and_flagged() {
let e = Endpoint {
kind: "future_transport_v9".into(),
url: "future:alien@mars:443".into(),
priority: 5,
health_hint_unix: None,
};
assert!(!e.is_known_kind());
}
#[test]
fn serde_roundtrip_minimal() {
let original = Endpoint::cloudflare_quick("https://x.trycloudflare.com");
let json = serde_json::to_string(&original).unwrap();
assert!(json.contains(r#""kind":"cloudflare_quick""#));
assert!(json.contains(r#""url":"https://x.trycloudflare.com""#));
assert!(!json.contains("health_hint_unix"));
let back: Endpoint = serde_json::from_str(&json).unwrap();
assert_eq!(back, original);
}
#[test]
fn serde_roundtrip_with_health_hint() {
let original = Endpoint {
kind: Endpoint::KIND_IROH.into(),
url: "iroh:abc123@relay.aex.dev:443".into(),
priority: 1,
health_hint_unix: Some(1_700_000_000),
};
let json = serde_json::to_string(&original).unwrap();
assert!(json.contains(r#""health_hint_unix":1700000000"#));
let back: Endpoint = serde_json::from_str(&json).unwrap();
assert_eq!(back, original);
}
#[test]
fn deserialize_preserves_unknown_kind() {
let json = r#"{"kind":"unknown_transport","url":"x://y","priority":9}"#;
let e: Endpoint = serde_json::from_str(json).unwrap();
assert_eq!(e.kind, "unknown_transport");
assert!(!e.is_known_kind());
}
#[test]
fn priority_defaults_to_zero_when_missing() {
let json = r#"{"kind":"cloudflare_quick","url":"https://x.trycloudflare.com"}"#;
let e: Endpoint = serde_json::from_str(json).unwrap();
assert_eq!(e.priority, 0);
assert_eq!(e.health_hint_unix, None);
}
#[test]
fn known_kinds_covers_sprint_2_transports() {
for k in [
Endpoint::KIND_CLOUDFLARE_QUICK,
Endpoint::KIND_CLOUDFLARE_NAMED,
Endpoint::KIND_IROH,
Endpoint::KIND_TAILSCALE_FUNNEL,
Endpoint::KIND_FRP,
] {
assert!(Endpoint::KNOWN_KINDS.contains(&k), "kind {k} missing");
}
}
}