rs-zero 0.2.10

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

use serde::Deserialize;

use crate::core::{
    ConfigFeatureWarning, CoreError, CoreResult, DatabaseSection, LogConfig, LogSection,
    RpcClientSection, ServiceConfig, dependency_feature_warnings, load_config,
};

/// RPC service configuration loaded by generated services.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct RpcServiceConfig {
    /// Service name.
    pub name: String,
    /// Deployment mode.
    pub mode: String,
    /// Server listener and timeout settings.
    pub server: RpcServerSection,
    /// Logging configuration.
    pub log: LogSection,
    /// Middleware toggles.
    pub middlewares: RpcMiddlewaresSection,
    /// Outbound RPC clients used by this RPC service.
    pub rpc_clients: BTreeMap<String, RpcClientSection>,
    /// Optional database used by model repositories.
    pub database: Option<DatabaseSection>,
}

impl Default for RpcServiceConfig {
    fn default() -> Self {
        let service = ServiceConfig::default();
        Self {
            name: service.name,
            mode: service.mode,
            server: RpcServerSection::default(),
            log: service.log,
            middlewares: RpcMiddlewaresSection::default(),
            rpc_clients: BTreeMap::new(),
            database: None,
        }
    }
}

impl RpcServiceConfig {
    /// Loads RPC service configuration from `basename` and env vars with `env_prefix`.
    pub fn load(basename: &str, env_prefix: &str) -> Result<Self, config::ConfigError> {
        load_config(basename, env_prefix)
    }

    /// Returns the listening address.
    pub fn addr(&self) -> CoreResult<SocketAddr> {
        format!("{}:{}", self.server.host, self.server.port)
            .parse()
            .map_err(|error| {
                config::ConfigError::Message(format!("invalid RPC listen address: {error}")).into()
            })
    }

    /// Converts to runtime logging config.
    pub fn log_config(&self) -> LogConfig {
        self.log.to_log_config(&self.name)
    }

    /// Returns warnings for config options that are unavailable in this Cargo feature build.
    pub fn validate_features(&self) -> Vec<ConfigFeatureWarning> {
        let mut warnings = Vec::new();
        if self.middlewares.resilience && !cfg!(feature = "resil") {
            warnings.push(ConfigFeatureWarning::ignored(
                "middlewares.resilience",
                "resil",
            ));
        }
        if self.middlewares.streaming && !cfg!(feature = "resil") {
            warnings.push(ConfigFeatureWarning::ignored(
                "middlewares.streaming",
                "resil",
            ));
        }
        warnings.extend(dependency_feature_warnings(
            &self.rpc_clients,
            self.database.as_ref(),
        ));
        warnings
    }

    /// Converts to runtime RPC server config.
    pub fn rpc_server_config(&self) -> CoreResult<crate::rpc::RpcServerConfig> {
        let mut config = if self.middlewares.resilience {
            crate::rpc::RpcServerConfig::production_defaults(self.name.clone(), self.addr()?)
        } else {
            crate::rpc::RpcServerConfig::new(self.name.clone(), self.addr()?)
        };
        if !self.middlewares.streaming {
            config.streaming = crate::rpc::RpcStreamingConfig::default();
        }
        Ok(config)
    }

    /// Returns one outbound RPC client dependency by logical name.
    pub fn rpc_client(&self, name: &str) -> CoreResult<&RpcClientSection> {
        self.rpc_clients.get(name).ok_or_else(|| {
            CoreError::Config(config::ConfigError::Message(format!(
                "missing rpc client config: {name}"
            )))
        })
    }

    /// Converts one outbound RPC client dependency to runtime RPC config.
    #[cfg(feature = "rpc")]
    pub fn rpc_client_config(&self, name: &str) -> CoreResult<crate::rpc::RpcClientConfig> {
        self.rpc_client(name)?.to_rpc_client_config()
    }

    /// Converts the optional database dependency to runtime database config.
    #[cfg(feature = "db")]
    pub fn database_config(&self) -> Option<crate::db::DatabaseConfig> {
        self.database
            .as_ref()
            .map(DatabaseSection::to_database_config)
    }
}

/// RPC listener section.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct RpcServerSection {
    pub host: String,
    pub port: u16,
    pub timeout_ms: u64,
    pub health: bool,
}

impl Default for RpcServerSection {
    fn default() -> Self {
        Self {
            host: "127.0.0.1".to_string(),
            port: 50051,
            timeout_ms: 5000,
            health: true,
        }
    }
}

/// RPC middleware toggles.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct RpcMiddlewaresSection {
    pub resilience: bool,
    pub streaming: bool,
}

impl Default for RpcMiddlewaresSection {
    fn default() -> Self {
        Self {
            resilience: true,
            streaming: true,
        }
    }
}

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

    #[test]
    fn validate_features_reflects_compile_time_features() {
        let warnings = RpcServiceConfig::default().validate_features();
        assert_eq!(
            warnings
                .iter()
                .any(|warning| warning.option == "middlewares.resilience"),
            !cfg!(feature = "resil")
        );
        assert_eq!(
            warnings
                .iter()
                .any(|warning| warning.option == "middlewares.streaming"),
            !cfg!(feature = "resil")
        );
    }
}