nacos-sdk 0.8.0

Nacos client in Rust.
Documentation
use std::collections::HashMap;

use crate::api::constants::*;
use crate::properties::{get_value, get_value_bool, get_value_option};

/// Configures settings for Client.
#[derive(Debug, Clone)]
pub struct ClientProps {
    /// server_addr e.g: 127.0.0.1:8848; 192.168.0.1
    server_addr: String,
    /// endpoint for resolving server list.
    /// Full URL (http://...) used as-is; bare hostname gets defaults (/nacos/serverlist, port 8080).
    endpoint: Option<String>,
    /// grpc port
    grpc_port: Option<u16>,
    /// public is "", Should define a more meaningful namespace
    namespace: String,
    /// app_name
    app_name: String,
    /// naming push_empty_protection, default true
    naming_push_empty_protection: bool,
    /// naming load_cache_at_start, default false
    naming_load_cache_at_start: bool,
    /// config load_cache_at_start, default false
    config_load_cache_at_start: bool,
    /// env_first when get props, default true
    env_first: bool,
    /// metadata
    labels: HashMap<String, String>,
    /// client_version
    client_version: String,
    /// auth context
    auth_context: HashMap<String, String>,
    /// Maximum retry attempts during initialization phase. Defaults to 1 when None.
    /// Only applies to the connection initialization stage. After successful initialization,
    /// the client will retry indefinitely during runtime to ensure fault tolerance.
    #[deprecated]
    max_retries: Option<u32>,
}

impl ClientProps {
    pub(crate) fn get_server_addr(&self) -> String {
        if self.env_first {
            get_value(
                ENV_NACOS_CLIENT_COMMON_SERVER_ADDRESS,
                self.server_addr.clone(),
            )
        } else {
            self.server_addr.clone()
        }
    }

    pub(crate) fn get_endpoint(&self) -> Option<String> {
        if self.env_first {
            get_value_option(ENV_NACOS_CLIENT_COMMON_ENDPOINT).or_else(|| self.endpoint.clone())
        } else {
            self.endpoint.clone()
        }
    }

    /// The priority of the `endpoint` is higher than `server_addr`
    pub(crate) fn get_address_identifier(&self) -> String {
        self.get_endpoint()
            .unwrap_or_else(|| self.get_server_addr())
    }

    pub(crate) fn get_remote_grpc_port(&self) -> Option<u16> {
        self.grpc_port
    }

    pub(crate) fn get_namespace_default_if_empty(&self) -> String {
        let namespace = self.get_namespace();
        if namespace.is_empty() {
            DEFAULT_NAMESPACE.to_owned()
        } else {
            namespace
        }
    }

    pub(crate) fn get_namespace(&self) -> String {
        if self.env_first {
            get_value(ENV_NACOS_CLIENT_COMMON_NAMESPACE, self.namespace.clone())
        } else {
            self.namespace.clone()
        }
    }

    pub(crate) fn get_app_name(&self) -> String {
        if self.env_first {
            get_value(ENV_NACOS_CLIENT_COMMON_APP_NAME, self.app_name.clone())
        } else {
            self.app_name.clone()
        }
    }

    pub(crate) fn get_naming_push_empty_protection(&self) -> bool {
        if self.env_first {
            get_value_bool(
                ENV_NACOS_CLIENT_NAMING_PUSH_EMPTY_PROTECTION,
                self.naming_push_empty_protection,
            )
        } else {
            self.naming_push_empty_protection
        }
    }

    pub(crate) fn get_naming_load_cache_at_start(&self) -> bool {
        if self.env_first {
            get_value_bool(
                ENV_NACOS_CLIENT_NAMING_LOAD_CACHE_AT_START,
                self.naming_load_cache_at_start,
            )
        } else {
            self.naming_load_cache_at_start
        }
    }

    pub(crate) fn get_config_load_cache_at_start(&self) -> bool {
        if self.env_first {
            get_value_bool(
                ENV_NACOS_CLIENT_CONFIG_LOAD_CACHE_AT_START,
                self.config_load_cache_at_start,
            )
        } else {
            self.config_load_cache_at_start
        }
    }

    pub(crate) fn get_labels(&self) -> HashMap<String, String> {
        let mut labels = self.labels.clone();
        labels.insert(KEY_LABEL_APP_NAME.to_string(), self.get_app_name());
        labels
    }

    pub(crate) fn get_client_version(&self) -> String {
        self.client_version.clone()
    }

    pub(crate) fn get_auth_context(&self) -> HashMap<String, String> {
        let mut auth_context = self.auth_context.clone();
        if self.env_first {
            #[cfg(feature = "auth-by-http")]
            self.get_http_auth_context(&mut auth_context);
            #[cfg(feature = "auth-by-aliyun")]
            self.get_aliyun_auth_context(&mut auth_context);
        }
        auth_context
    }

    #[cfg(feature = "auth-by-http")]
    fn get_http_auth_context(&self, context: &mut HashMap<String, String>) {
        if let Some(u) = get_value_option(ENV_NACOS_CLIENT_AUTH_USERNAME) {
            context.insert(crate::api::plugin::USERNAME.into(), u);
        }
        if let Some(p) = get_value_option(ENV_NACOS_CLIENT_AUTH_PASSWORD) {
            context.insert(crate::api::plugin::PASSWORD.into(), p);
        }
    }

    #[cfg(feature = "auth-by-aliyun")]
    fn get_aliyun_auth_context(&self, context: &mut HashMap<String, String>) {
        if let Some(ak) = get_value_option(ENV_NACOS_CLIENT_AUTH_ACCESS_KEY) {
            context.insert(crate::api::plugin::ACCESS_KEY.into(), ak);
        }
        if let Some(sk) = get_value_option(ENV_NACOS_CLIENT_AUTH_ACCESS_SECRET) {
            context.insert(crate::api::plugin::ACCESS_SECRET.into(), sk);
        }
        if let Some(sign_region_id) = get_value_option(ENV_NACOS_CLIENT_SIGN_REGION_ID) {
            context.insert(crate::api::plugin::SIGN_REGION_ID.into(), sign_region_id);
        }
    }

    pub(crate) fn get_max_retries(&self) -> Option<u32> {
        #[allow(deprecated)]
        self.max_retries
    }
}

#[allow(clippy::new_without_default)]
impl ClientProps {
    /// Creates a new `ClientConfig`.
    pub fn new() -> Self {
        let env_project_version = env!("CARGO_PKG_VERSION");
        let client_version = format!("Nacos-Rust-Client:{}", env_project_version);

        ClientProps {
            server_addr: String::from(DEFAULT_SERVER_ADDR),
            endpoint: None,
            namespace: String::from(""),
            app_name: UNKNOWN.to_string(),
            naming_push_empty_protection: true,
            naming_load_cache_at_start: false,
            config_load_cache_at_start: false,
            env_first: true,
            labels: HashMap::default(),
            client_version,
            auth_context: HashMap::default(),
            grpc_port: None,
            #[allow(deprecated)]
            max_retries: None,
        }
    }

    /// Sets the server addr.
    pub fn server_addr(mut self, server_addr: impl Into<String>) -> Self {
        self.server_addr = server_addr.into();
        self
    }

    /// Sets the endpoint used to resolve server addresses.
    ///
    /// Full URL (e.g. `http://addr:8080/nacos/serverlist`) is used as-is,
    /// only appending `namespace` if missing from the query string.
    /// Bare hostname (e.g. `addr` or `addr:9090`) gets default path
    /// `/nacos/serverlist` and port 8080.
    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
        self.endpoint = Some(endpoint.into());
        self
    }

    /// Sets the grpc port
    pub fn remote_grpc_port(mut self, grpc_port: u16) -> Self {
        self.grpc_port = Some(grpc_port);
        self
    }

    /// Sets the namespace.
    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
        self.namespace = namespace.into();
        self
    }

    /// Sets the app_name.
    pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
        self.app_name = app_name.into();
        self
    }

    /// Sets the naming_push_empty_protection.
    pub fn naming_push_empty_protection(mut self, naming_push_empty_protection: bool) -> Self {
        self.naming_push_empty_protection = naming_push_empty_protection;
        self
    }

    /// Sets the naming_load_cache_at_start.
    pub fn naming_load_cache_at_start(mut self, naming_load_cache_at_start: bool) -> Self {
        self.naming_load_cache_at_start = naming_load_cache_at_start;
        self
    }

    /// Sets the config_load_cache_at_start.
    pub fn config_load_cache_at_start(mut self, config_load_cache_at_start: bool) -> Self {
        self.config_load_cache_at_start = config_load_cache_at_start;
        self
    }

    /// Sets the config_load_cache_at_start / naming_load_cache_at_start.
    pub fn load_cache_at_start(mut self, load_cache_at_start: bool) -> Self {
        self.naming_load_cache_at_start = load_cache_at_start;
        self.config_load_cache_at_start = load_cache_at_start;
        self
    }

    /// Sets the env_first.
    pub fn env_first(mut self, env_first: bool) -> Self {
        self.env_first = env_first;
        self
    }

    /// Sets the labels.
    pub fn labels(mut self, labels: HashMap<String, String>) -> Self {
        self.labels.extend(labels);
        self
    }

    /// Add auth username.
    #[cfg(feature = "auth-by-http")]
    pub fn auth_username(mut self, username: impl Into<String>) -> Self {
        self.auth_context
            .insert(crate::api::plugin::USERNAME.into(), username.into());
        self
    }

    /// Add auth password.
    #[cfg(feature = "auth-by-http")]
    pub fn auth_password(mut self, password: impl Into<String>) -> Self {
        self.auth_context
            .insert(crate::api::plugin::PASSWORD.into(), password.into());
        self
    }

    /// Add access-key
    #[cfg(feature = "auth-by-aliyun")]
    pub fn auth_access_key(mut self, access_key: impl Into<String>) -> Self {
        self.auth_context
            .insert(crate::api::plugin::ACCESS_KEY.into(), access_key.into());
        self
    }

    /// Add access-secret
    #[cfg(feature = "auth-by-aliyun")]
    pub fn auth_access_secret(mut self, access_secret: impl Into<String>) -> Self {
        self.auth_context.insert(
            crate::api::plugin::ACCESS_SECRET.into(),
            access_secret.into(),
        );
        self
    }

    /// Add signature region id
    #[cfg(feature = "auth-by-aliyun")]
    pub fn auth_signature_region_id(mut self, signature_region_id: impl Into<String>) -> Self {
        self.auth_context.insert(
            crate::api::plugin::SIGN_REGION_ID.into(),
            signature_region_id.into(),
        );
        self
    }

    /// Add auth ext params.
    pub fn auth_ext(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
        self.auth_context.insert(key.into(), val.into());
        self
    }

    /// Sets the maximum retry attempts during initialization phase.
    ///
    /// This value only applies to the connection initialization stage.
    /// If not set, defaults to 1 retry attempts.
    /// After successful initialization, the client will retry indefinitely
    /// during runtime to ensure fault tolerance.
    #[deprecated]
    #[allow(deprecated)]
    pub fn max_retries(mut self, max_retries: u32) -> Self {
        self.max_retries = Some(max_retries);
        self
    }
}

#[cfg(test)]
mod tests {
    use crate::api::error::Error;

    use super::*;

    #[tokio::test]
    async fn test_get_server_list() {
        let client_props = ClientProps::new()
            .server_addr("127.0.0.1:8848,192.168.0.1")
            .namespace("test_namespace");

        let provider =
            crate::common::remote::server_list::create_server_list_provider(&client_props)
                .await
                .expect("provider should be created");
        let result = provider.current_server_list().await;
        assert!(result.contains(&"127.0.0.1:8848".to_string()));
        assert!(result.contains(&"192.168.0.1:8848".to_string()));

        let client_props = ClientProps::new().server_addr("     ");
        let result1 =
            crate::common::remote::server_list::create_server_list_provider(&client_props).await;
        assert!(result1.is_err());
        let err = match result1 {
            Ok(_) => panic!("expected error result"),
            Err(err) => err,
        };
        assert_eq!(
            format!("{}", err),
            format!(
                "{}",
                Error::WrongServerAddress("Server address is empty".to_string())
            )
        );
    }

    #[test]
    fn test_get_endpoint() {
        let props = ClientProps::new().endpoint("http://127.0.0.1:8080");
        assert_eq!(
            props.get_endpoint().as_deref(),
            Some("http://127.0.0.1:8080")
        );
    }

    #[test]
    fn test_address_identifier_prefers_endpoint() {
        let props = ClientProps::new()
            .server_addr("10.0.0.1:8848,10.0.0.2:8848,10.0.0.3:8848")
            .endpoint("http://endpoint.example.com:8080/nacos/serverlist");
        assert_eq!(
            props.get_address_identifier(),
            "http://endpoint.example.com:8080/nacos/serverlist"
        );

        let props = ClientProps::new().server_addr("10.0.0.1:8848");
        assert_eq!(props.get_address_identifier(), "10.0.0.1:8848");
    }
}