serverstat 0.1.4

Get server info from QuakeWorld servers.
Documentation
use anyhow::Result;
use std::time::Duration;

pub use quake_serverinfo::Settings;

use crate::client::QuakeClient;
use crate::hostport::Hostport;
use crate::qtv::QtvStream;
use crate::server_type::ServerType;
use crate::software_type::SoftwareType;
use crate::svc_status;
use crate::{net_extra, svc_qtvusers};

#[cfg(feature = "json")]
use {
    crate::gameserver::GameServer,
    crate::qtv::QtvServer,
    crate::qwfwd::QwfwdServer,
    serde::{Serialize, Serializer, ser::SerializeStruct},
};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuakeServer {
    pub server_type: ServerType,
    pub software_type: SoftwareType,
    pub address: Hostport,
    pub ip: String,
    pub settings: Settings,
    pub clients: Vec<QuakeClient>,
    pub qtv_stream: Option<QtvStream>,
}

impl QuakeServer {
    pub async fn try_from_address(address: &str, timeout: Duration) -> Result<Self> {
        let mut res = svc_status::status_119(address, timeout).await?;
        let ip = net_extra::address_to_ip(address).unwrap_or_default();

        res.qtv_stream = match res.qtv_stream {
            Some(qtv_stream) => {
                let res = svc_qtvusers::qtvusers(address, timeout)
                    .await
                    .unwrap_or_default();
                Some(QtvStream {
                    client_names: res.client_names,
                    ..qtv_stream
                })
            }
            None => None,
        };

        let address = {
            let address_str = res.settings.clone().hostport.unwrap_or(address.to_string());
            Hostport::try_from(address_str.as_str())?
        };
        let version = res.settings.version.as_deref().unwrap_or("");

        Ok(QuakeServer {
            server_type: ServerType::from_version(version),
            software_type: SoftwareType::from_version(version),
            address,
            ip,
            settings: res.settings,
            clients: res.clients,
            qtv_stream: res.qtv_stream,
        })
    }
}

#[cfg(feature = "json")]
impl Serialize for QuakeServer {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let field_count: usize = 6 + match self.software_type {
            SoftwareType::Qtv | SoftwareType::Qwfwd => 2,
            _ => 5,
        };

        let mut state = serializer.serialize_struct("QuakeServer", field_count)?;
        state.serialize_field("server_type", &self.server_type)?;
        state.serialize_field("software_type", &self.software_type)?;
        state.serialize_field("host", &self.address.host)?;
        state.serialize_field("ip", &self.ip)?;
        state.serialize_field("port", &self.address.port)?;
        state.serialize_field("address", &self.address)?;

        if self.software_type == SoftwareType::Qtv {
            let qtv = QtvServer::from(self);
            state.serialize_field("settings", &qtv.settings)?;
            state.serialize_field("clients", &qtv.clients)?;
        } else if self.software_type == SoftwareType::Qwfwd {
            let qwfwd = QwfwdServer::from(self);
            state.serialize_field("settings", &qwfwd.settings)?;
            state.serialize_field("clients", &qwfwd.clients)?;
        } else {
            let server = GameServer::from(self);
            state.serialize_field("settings", &server.settings)?;
            state.serialize_field("teams", &server.teams)?;
            state.serialize_field("players", &server.players)?;
            state.serialize_field("spectators", &server.spectators)?;
            state.serialize_field("qtv_stream", &server.qtv_stream)?;
        }

        state.end()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;
    use pretty_assertions::assert_eq;

    #[tokio::test]
    async fn test_try_from_address() -> Result<()> {
        assert!(
            QuakeServer::try_from_address("foo.bar:666", Duration::from_millis(50))
                .await
                .is_err()
        );
        let server =
            QuakeServer::try_from_address("quake.se:28501", Duration::from_secs_f32(0.5)).await?;

        assert!(
            server
                .clone()
                .settings
                .hostname
                .unwrap()
                .starts_with("QUAKE.SE KTX:28501")
        );
        assert_eq!(
            server.address,
            Hostport {
                host: "quake.se".to_string(),
                port: 28501,
            }
        );
        Ok(())
    }
}