deepslate 0.1.0

A high-performance Minecraft server proxy written in Rust.
Documentation
//! Named server registry with ordered try-list for initial server selection.

use std::sync::RwLock;

use tracing::info;

/// A single upstream backend server.
#[derive(Debug, Clone)]
pub struct Server {
    /// Unique identifier (e.g., "lobby", "survival").
    pub id: String,
    /// Upstream address (host:port).
    pub addr: String,
}

impl Server {
    /// Create a new server with the given ID and address.
    #[must_use]
    pub fn new(id: impl Into<String>, addr: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            addr: addr.into(),
        }
    }
}

/// Thread-safe registry of named backend servers.
///
/// Servers are registered by ID and looked up by name. A configurable
/// "try" list determines the order in which servers are tried for a
/// player's initial connection.
pub struct ServerRegistry {
    servers: RwLock<Vec<Server>>,
    try_order: RwLock<Vec<String>>,
}

impl ServerRegistry {
    /// Create an empty server registry.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            servers: RwLock::new(Vec::new()),
            try_order: RwLock::new(Vec::new()),
        }
    }

    /// Register a new server.
    /// Returns `true` if the server was added, `false` if a server with the
    /// same ID already exists.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn register(&self, server: &Server) -> bool {
        let mut servers = self.servers.write().unwrap();
        if servers.iter().any(|s| s.id == server.id) {
            return false;
        }

        servers.push(server.clone());
        drop(servers);

        info!(id = server.id, addr = server.addr, "registered server");

        true
    }

    /// Deregister a server by ID.
    /// Returns the removed server, or `None` if not found.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn deregister(&self, id: &str) -> Option<Server> {
        let mut servers = self.servers.write().unwrap();
        servers
            .iter()
            .position(|s| s.id == id)
            .map(|pos| servers.remove(pos))
    }

    /// Look up a server by ID.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn get(&self, id: &str) -> Option<Server> {
        let servers = self.servers.read().unwrap();
        servers.iter().find(|s| s.id == id).cloned()
    }

    /// List all registered servers.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn list(&self) -> Vec<Server> {
        self.servers.read().unwrap().clone()
    }

    /// Set the try order for initial server selection.
    ///
    /// The try list is an ordered list of server IDs. When a player first
    /// connects, the proxy tries each server in order until one is found
    /// in the registry.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn set_try_order(&self, ids: Vec<String>) {
        *self.try_order.write().unwrap() = ids;
    }

    /// Get the current try order.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn try_order(&self) -> Vec<String> {
        self.try_order.read().unwrap().clone()
    }

    /// Select the first available server from the try list.
    ///
    /// Iterates the try order and returns the first server that exists
    /// in the registry. Returns `None` if no try-list server is registered.
    ///
    /// # Panics
    ///
    /// Panics if the internal `RwLock` is poisoned.
    pub fn select_initial(&self) -> Option<Server> {
        let try_order = self.try_order.read().unwrap().clone();
        let servers = self.servers.read().unwrap();

        for id in &try_order {
            if let Some(server) = servers.iter().find(|s| s.id == *id) {
                return Some(server.clone());
            }
        }

        // Fallback: if the try list is empty or has no matches, return the first
        // registered server (if any).
        servers.first().cloned()
    }
}

impl Default for ServerRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_register_and_list() {
        let reg = ServerRegistry::new();
        let server = Server::new("lobby", "127.0.0.1:25565");

        assert!(reg.register(&server));
        assert!(!reg.register(&server)); // Duplicate

        let servers = reg.list();
        assert_eq!(servers.len(), 1);
        assert_eq!(servers[0].id, "lobby");
    }

    #[test]
    fn test_deregister() {
        let reg = ServerRegistry::new();
        reg.register(&Server::new("lobby", "127.0.0.1:25565"));

        let removed = reg.deregister("lobby");
        assert!(removed.is_some());
        assert_eq!(removed.unwrap().id, "lobby");
        assert!(reg.list().is_empty());

        assert!(reg.deregister("nonexistent").is_none());
    }

    #[test]
    fn test_get() {
        let reg = ServerRegistry::new();
        reg.register(&Server::new("lobby", "127.0.0.1:25565"));
        reg.register(&Server::new("survival", "127.0.0.1:25566"));

        let server = reg.get("survival").unwrap();
        assert_eq!(server.addr, "127.0.0.1:25566");

        assert!(reg.get("nonexistent").is_none());
    }

    #[test]
    fn test_try_order_selection() {
        let reg = ServerRegistry::new();
        reg.register(&Server::new("survival", "127.0.0.1:25566"));
        reg.register(&Server::new("lobby", "127.0.0.1:25565"));

        // Without a try order, fallback returns the first registered server
        assert_eq!(reg.select_initial().unwrap().id, "survival");

        // With a try order, it respects the order
        reg.set_try_order(vec!["lobby".to_string(), "survival".to_string()]);
        assert_eq!(reg.select_initial().unwrap().id, "lobby");
    }

    #[test]
    fn test_try_order_skips_missing() {
        let reg = ServerRegistry::new();
        reg.register(&Server::new("survival", "127.0.0.1:25566"));

        // "lobby" isn't registered, so it skips to "survival"
        reg.set_try_order(vec!["lobby".to_string(), "survival".to_string()]);
        assert_eq!(reg.select_initial().unwrap().id, "survival");
    }

    #[test]
    fn test_select_initial_empty() {
        let reg = ServerRegistry::new();
        assert!(reg.select_initial().is_none());
    }

    #[test]
    fn test_try_order_roundtrip() {
        let reg = ServerRegistry::new();
        let order = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        reg.set_try_order(order.clone());
        assert_eq!(reg.try_order(), order);
    }
}