eureka-client 0.0.4

Handles HTTP requests to and registering a service with Eureka
Documentation
extern crate itertools;
#[macro_use]
extern crate log;
extern crate percent_encoding;
#[macro_use]
extern crate quick_error;
extern crate reqwest;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use self::instance::InstanceClient;
pub use self::instance::{Instance, PortData, StatusType};
use self::registry::RegistryClient;
use reqwest::header::HeaderMap;
use reqwest::Client as ReqwestClient;
pub use reqwest::{Error as ReqwestError, Method, Response, StatusCode};
use serde::Serialize;

mod aws;
mod instance;
mod registry;
mod resolver;
mod rest;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EurekaConfig {
    pub host: String,
    pub port: u16,
    pub heartbeat_interval: usize,
    pub registry_fetch_interval: usize,
    pub max_retries: usize,
    pub request_retry_delay: usize,
    pub fetch_registry: bool,
    pub filter_up_instances: bool,
    pub service_path: String,
    pub ssl: bool,
    pub use_dns: bool,
    pub prefer_same_zone: bool,
    pub cluster_refresh_interval: usize,
    pub fetch_metadata: bool,
    pub register_with_eureka: bool,
    pub use_local_metadata: bool,
    pub prefer_ip_address: bool,
}

impl Default for EurekaConfig {
    fn default() -> Self {
        EurekaConfig {
            host: "localhost".to_string(),
            port: 8761,
            heartbeat_interval: 30_000,
            registry_fetch_interval: 30_000,
            max_retries: 3,
            request_retry_delay: 500,
            fetch_registry: true,
            filter_up_instances: true,
            service_path: "/eureka".to_string(),
            ssl: false,
            use_dns: false,
            prefer_same_zone: true,
            cluster_refresh_interval: 300_000,
            fetch_metadata: true,
            register_with_eureka: true,
            use_local_metadata: false,
            prefer_ip_address: false,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BaseConfig {
    pub eureka: EurekaConfig,
    pub instance: Instance,
}

quick_error! {
    #[derive(Debug)]
    pub enum EurekaError {
        Network(err: ReqwestError) {
            description(err.description())
            cause(err)
        }
        Request(status: StatusCode) {
            description(status.canonical_reason().unwrap_or("Unknown Status Code"))
        }
        UnexpectedState(description: String) {
            description(description)
        }
        ParseError(description: String) {}
    }
}

#[derive(Debug)]
pub struct EurekaClient {
    base_url: String,
    config: BaseConfig,
    client: ReqwestClient,
    registry: RegistryClient,
    instance: Option<InstanceClient>,
}

impl EurekaClient {
    pub fn new(config: BaseConfig) -> Self {
        let base_url = {
            let ssl = config.eureka.ssl;
            let protocol = if ssl { "https" } else { "http" };
            let host = &config.eureka.host;
            let port = config.eureka.port;
            let service_path = &config.eureka.service_path;
            format!("{}://{}:{}{}", protocol, host, port, service_path)
        };
        EurekaClient {
            base_url: base_url.clone(),
            client: ReqwestClient::new(),
            registry: RegistryClient::new(base_url.clone()),
            instance: if config.eureka.register_with_eureka {
                Some(InstanceClient::new(base_url, config.instance.clone()))
            } else {
                None
            },
            config,
        }
    }

    pub fn start(&self) {
        self.registry.start();
        if let Some(ref instance) = self.instance {
            instance.start();
        }
    }

    /// Sends a request to another app in this eureka cluster, and returns the response.
    ///
    /// This method assumes that your services all communicate using JSON.
    /// Future methods may be added to allow other request body types.
    ///
    /// `Accept: "application/json"` is preset on all requests by this method.
    /// You can add additional headers such as `Authorization` using the `headers` parameter.
    pub fn make_request<V: Serialize>(
        &self,
        app: &str,
        path: &str,
        method: Method,
        body: &V,
        mut headers: HeaderMap,
    ) -> Result<Response, EurekaError> {
        let instance = self.registry.get_instance_by_app_name(app);
        if let Some(instance) = instance {
            let ssl = self.config.eureka.ssl;
            let protocol = if ssl { "https" } else { "http" };
            let host = instance.ip_addr;
            let port = if ssl && instance.secure_port.value().is_some() {
                instance.secure_port.value().unwrap()
            } else {
                instance.port.and_then(|port| port.value()).unwrap_or(8080)
            };
            headers.insert("Accept", "application/json".parse().unwrap());
            self.client
                .request(
                    method,
                    &format!(
                        "{}://{}:{}/{}",
                        protocol,
                        host,
                        port,
                        path.trim_left_matches('/')
                    ),
                ).headers(headers)
                .json(body)
                .send()
                .map_err(EurekaError::Network)
        } else {
            Err(EurekaError::UnexpectedState(format!(
                "Could not find app {}",
                app
            )))
        }
    }
}

fn path_segment_encode(value: &str) -> String {
    percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET)
        .to_string()
}

fn query_encode(value: &str) -> String {
    percent_encoding::utf8_percent_encode(value, percent_encoding::QUERY_ENCODE_SET).to_string()
}