rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Tower [`Service`] integration for [`McClient`].
//!
//! Only compiled when the `tower` feature is enabled.
//!
//! # Quick start
//!
//! ```rust,no_run
//! use std::time::Duration;
//! use tower::{Service, ServiceBuilder, ServiceExt};
//! use rust_mc_status::{McClient, McRetryPolicy, PingRequestTower};
//!
//! # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
//! // Compose middleware around the McService. Layers apply bottom-up:
//! // the rate limiter sees every request, the retry policy reissues on
//! // retryable errors, and the inner service does the actual ping.
//! let mut svc = ServiceBuilder::new()
//!     .rate_limit(100, Duration::from_secs(1))
//!     .retry(McRetryPolicy::new(3))
//!     .service(McClient::builder().timeout(Duration::from_secs(10)).build().into_service());
//!
//! svc.ready().await?;
//! let resp = svc.call(PingRequestTower::java("mc.hypixel.net")).await?;
//! println!("latency: {:.0} ms", resp.status.latency);
//! # Ok(()) }
//! ```

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

use tower::Service;

use crate::client::McClient;
use crate::error::McError;
use crate::models::{ServerEdition, ServerStatus};

// ─── Request / Response ───────────────────────────────────────────────────────

/// A ping request passed to [`McService`] via `tower::Service::call`.
#[derive(Debug, Clone)]
pub struct PingRequestTower {
    /// Target server address, e.g. `"mc.hypixel.net"` or `"192.168.1.1:25565"`.
    pub address: String,
    /// Which edition to ping.
    pub edition: ServerEdition,
}

impl PingRequestTower {
    /// Create a Java Edition ping request.
    pub fn java(address: impl Into<String>) -> Self {
        Self { address: address.into(), edition: ServerEdition::Java }
    }

    /// Create a Bedrock Edition ping request.
    pub fn bedrock(address: impl Into<String>) -> Self {
        Self { address: address.into(), edition: ServerEdition::Bedrock }
    }
}

/// The response returned by [`McService`].
#[derive(Debug)]
pub struct PingResponseTower {
    /// The full server status.  Match on `status.data` or use
    /// [`ServerStatus::players`](crate::ServerStatus::players) for edition-agnostic access.
    /// For typed accessors wrap in [`JavaServerStatus`](crate::JavaServerStatus) or
    /// [`BedrockServerStatus`](crate::BedrockServerStatus).
    pub status: ServerStatus,
}

// ─── McService ────────────────────────────────────────────────────────────────

/// A [`tower::Service`] that pings Minecraft servers.
///
/// Wrap with [`ServiceBuilder`](tower::ServiceBuilder) to add rate-limiting,
/// retries, timeouts, tracing, and any other Tower middleware.
///
/// `McService` itself takes `&mut self` as required by `Service`. For
/// concurrent use, wrap with `tower::buffer::Buffer` (`.buffer(N)`) which
/// makes the service `Clone + Send`.
///
/// # Example
///
/// ```rust,no_run
/// use std::time::Duration;
/// use tower::{Service, ServiceBuilder, ServiceExt};
/// use rust_mc_status::{McClient, McRetryPolicy, PingRequestTower};
///
/// # #[tokio::main] async fn main() -> Result<(), rust_mc_status::McError> {
/// let mut svc = ServiceBuilder::new()
///     .rate_limit(50, Duration::from_secs(1))
///     .retry(McRetryPolicy::new(2))
///     .service(McClient::builder().build().into_service());
///
/// svc.ready().await?;
/// let resp = svc.call(PingRequestTower::java("mc.hypixel.net")).await?;
/// println!("latency: {:.0} ms", resp.status.latency);
/// # Ok(()) }
/// ```
#[derive(Clone)]
pub struct McService {
    client: McClient,
}

impl McService {
    /// Create a new `McService` wrapping the given client.
    ///
    /// Prefer [`McClient::into_service`] for ergonomics.
    pub fn new(client: McClient) -> Self {
        Self { client }
    }
}

impl Service<PingRequestTower> for McService {
    type Response = PingResponseTower;
    type Error    = McError;
    type Future   = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // McClient is always ready — no connection pool to exhaust.
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: PingRequestTower) -> Self::Future {
        let client = self.client.clone();
        Box::pin(async move {
            let status = match req.edition {
                ServerEdition::Java    => client.ping_java_inner(&req.address).await.map(|s| s.0),
                ServerEdition::Bedrock => client.ping_bedrock_inner(&req.address).await.map(|s| s.0),
            }?;
            Ok(PingResponseTower { status })
        })
    }
}

// ─── McRetryPolicy ────────────────────────────────────────────────────────────

/// Retry policy for use with [`tower::retry::Retry`].
///
/// Retries on [`NetworkError::Timeout`](crate::error::NetworkError::Timeout) and
/// [`NetworkError::Connection`](crate::error::NetworkError::Connection) only.
/// Protocol errors (`InvalidResponse`, `JsonError`, etc.) are not retried —
/// retrying them would just produce the same result.
///
/// # Example
///
/// ```rust,no_run
/// use tower::ServiceBuilder;
/// use rust_mc_status::{McClient, McRetryPolicy};
///
/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let svc = ServiceBuilder::new()
///     .retry(McRetryPolicy::new(3)) // up to 3 attempts total
///     .service(McClient::builder().build().into_service());
/// # Ok(()) }
/// ```
#[derive(Clone, Debug)]
pub struct McRetryPolicy {
    remaining: usize,
}

impl McRetryPolicy {
    /// Create a policy that allows up to `attempts` total tries (1 = no retry).
    pub fn new(attempts: usize) -> Self {
        Self { remaining: attempts.saturating_sub(1) }
    }
}

impl tower::retry::Policy<PingRequestTower, PingResponseTower, McError> for McRetryPolicy {
    type Future = std::future::Ready<()>;

    fn retry(
        &mut self,
        _req: &mut PingRequestTower,
        result: &mut Result<PingResponseTower, McError>,
    ) -> Option<Self::Future> {
        if self.remaining == 0 {
            return None;
        }
        let should_retry = match result {
            Err(McError::Network(crate::error::NetworkError::Timeout))      => true,
            Err(McError::Network(crate::error::NetworkError::Connection(_))) => true,
            Err(McError::Network(crate::error::NetworkError::Dns(_)))        => false, // DNS errors unlikely to resolve
            _                                  => false,
        };
        if should_retry {
            self.remaining -= 1;
            Some(std::future::ready(()))
        } else {
            None
        }
    }

    fn clone_request(&mut self, req: &PingRequestTower) -> Option<PingRequestTower> {
        Some(req.clone())
    }
}