rs-zero 0.2.6

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
use crate::discovery::ServiceInstance;

use crate::discovery_etcd::{EtcdDiscoveryConfig, EtcdDiscoveryError, EtcdDiscoveryResult};

/// Parsed service and instance id extracted from an etcd key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EtcdInstanceKey {
    /// Service name.
    pub service: String,
    /// Service instance id.
    pub id: String,
}

/// Builds the etcd key for one service instance.
pub fn instance_key(config: &EtcdDiscoveryConfig, service: &str, id: &str) -> String {
    format!(
        "{}/{}/{}",
        config.prefix.trim_end_matches('/'),
        service.trim_matches('/'),
        id.trim_matches('/')
    )
}

/// Builds the etcd prefix used to discover or watch a service.
pub fn service_prefix(config: &EtcdDiscoveryConfig, service: &str) -> String {
    format!(
        "{}/{}/",
        config.prefix.trim_end_matches('/'),
        service.trim_matches('/')
    )
}

/// Splits an etcd key into service and instance id when it belongs to the prefix.
pub fn split_instance_key(
    config: &EtcdDiscoveryConfig,
    key: &str,
) -> EtcdDiscoveryResult<Option<EtcdInstanceKey>> {
    let root = format!("{}/", config.prefix.trim_end_matches('/'));
    let Some(suffix) = key.strip_prefix(&root) else {
        return Ok(None);
    };
    let Some((service, id)) = suffix.split_once('/') else {
        return Err(EtcdDiscoveryError::InvalidKey(key.to_string()));
    };
    if service.trim().is_empty() || id.trim().is_empty() {
        return Err(EtcdDiscoveryError::InvalidKey(key.to_string()));
    }
    Ok(Some(EtcdInstanceKey {
        service: service.to_string(),
        id: id.to_string(),
    }))
}

/// Encodes an instance as JSON.
pub fn encode_instance(instance: &ServiceInstance) -> EtcdDiscoveryResult<Vec<u8>> {
    Ok(serde_json::to_vec(instance)?)
}

/// Decodes an instance from JSON.
pub fn decode_instance(value: &[u8]) -> EtcdDiscoveryResult<ServiceInstance> {
    Ok(serde_json::from_slice(value)?)
}

pub(crate) fn decode_healthy_instances(
    kvs: Vec<etcd_client::KeyValue>,
) -> EtcdDiscoveryResult<Vec<ServiceInstance>> {
    let mut instances = Vec::new();
    for kv in kvs {
        let instance = decode_instance(kv.value())?;
        if instance.healthy {
            instances.push(instance);
        }
    }
    Ok(instances)
}

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

    use crate::discovery_etcd::{
        EtcdDiscoveryConfig, decode_instance, encode_instance, instance_key, service_prefix,
        split_instance_key,
    };

    #[test]
    fn codec_round_trips_service_instance() {
        let instance = ServiceInstance::new(
            "api",
            "api-1",
            InstanceEndpoint::new("127.0.0.1", 8080).expect("endpoint"),
        )
        .with_metadata("zone", "local");
        let value = encode_instance(&instance).expect("encode");
        assert_eq!(decode_instance(&value).expect("decode"), instance);
        assert_eq!(
            instance_key(&EtcdDiscoveryConfig::default(), "api", "api-1"),
            "/rs-zero/api/api-1"
        );
        assert_eq!(
            service_prefix(&EtcdDiscoveryConfig::default(), "api"),
            "/rs-zero/api/"
        );
        let parsed = split_instance_key(&EtcdDiscoveryConfig::default(), "/rs-zero/api/api-1")
            .expect("split")
            .expect("parts");
        assert_eq!(parsed.service, "api");
        assert_eq!(parsed.id, "api-1");
    }
}