rs-zero 0.2.6

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
use std::{collections::BTreeMap, fmt, net::SocketAddr, str::FromStr};

use serde::{Deserialize, Serialize};

use crate::discovery::{DiscoveryError, DiscoveryResult};

/// Network endpoint for a service instance.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct InstanceEndpoint {
    host: String,
    port: u16,
}

impl InstanceEndpoint {
    /// Creates an endpoint from a host and port.
    pub fn new(host: impl Into<String>, port: u16) -> DiscoveryResult<Self> {
        let host = host.into();
        if host.trim().is_empty() {
            return Err(DiscoveryError::InvalidEndpoint { endpoint: host });
        }
        Ok(Self { host, port })
    }

    /// Returns the endpoint host.
    pub fn host(&self) -> &str {
        &self.host
    }

    /// Returns the endpoint port.
    pub fn port(&self) -> u16 {
        self.port
    }

    /// Returns a `host:port` string.
    pub fn authority(&self) -> String {
        format!("{}:{}", self.host, self.port)
    }

    /// Converts a socket address into an endpoint.
    pub fn from_socket_addr(addr: SocketAddr) -> Self {
        Self {
            host: addr.ip().to_string(),
            port: addr.port(),
        }
    }
}

impl fmt::Display for InstanceEndpoint {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}:{}", self.host, self.port)
    }
}

impl FromStr for InstanceEndpoint {
    type Err = DiscoveryError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let (host, port) =
            value
                .rsplit_once(':')
                .ok_or_else(|| DiscoveryError::InvalidEndpoint {
                    endpoint: value.to_string(),
                })?;
        let port = port.parse().map_err(|_| DiscoveryError::InvalidEndpoint {
            endpoint: value.to_string(),
        })?;
        Self::new(host, port)
    }
}

/// A discoverable service instance.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceInstance {
    /// Service name used by discovery clients.
    pub service: String,
    /// Stable instance id inside a service.
    pub id: String,
    /// Network endpoint.
    pub endpoint: InstanceEndpoint,
    /// Free-form metadata for adapters and clients.
    pub metadata: BTreeMap<String, String>,
    /// Whether this instance is considered healthy.
    pub healthy: bool,
    /// Relative weight used by selectors.
    pub weight: u32,
}

impl ServiceInstance {
    /// Creates a healthy service instance with weight `1`.
    pub fn new(
        service: impl Into<String>,
        id: impl Into<String>,
        endpoint: InstanceEndpoint,
    ) -> Self {
        Self {
            service: service.into(),
            id: id.into(),
            endpoint,
            metadata: BTreeMap::new(),
            healthy: true,
            weight: 1,
        }
    }

    /// Adds metadata and returns the updated instance.
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }

    /// Sets the health flag and returns the updated instance.
    pub fn with_health(mut self, healthy: bool) -> Self {
        self.healthy = healthy;
        self
    }

    /// Sets the selector weight. Zero is normalized to one.
    pub fn with_weight(mut self, weight: u32) -> Self {
        self.weight = weight.max(1);
        self
    }
}

#[cfg(test)]
mod tests {
    use super::{InstanceEndpoint, ServiceInstance};

    #[test]
    fn endpoint_parses_authority() {
        let endpoint: InstanceEndpoint = "127.0.0.1:8080".parse().expect("endpoint");
        assert_eq!(endpoint.host(), "127.0.0.1");
        assert_eq!(endpoint.port(), 8080);
        assert_eq!(endpoint.authority(), "127.0.0.1:8080");
    }

    #[test]
    fn service_instance_defaults_are_healthy() {
        let endpoint = InstanceEndpoint::new("localhost", 9000).expect("endpoint");
        let instance = ServiceInstance::new("user", "user-1", endpoint).with_weight(0);
        assert!(instance.healthy);
        assert_eq!(instance.weight, 1);
    }
}