boomack 0.4.1

Client library for Boomack
Documentation
//! Common structures and functions
//! for all kinds of requests to the HTTP API of Boomack

use std::time::Duration;
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs::File;
use std::io::stdin;
use ureq::{Agent, AgentBuilder};
pub use ureq::{Response, Error};
use super::config::Config;
use super::json::JsonMap;

/// Build an HTTP(S) URL from configuration and API route.
pub fn build_request_url(cfg: &Config, route: &str) -> String {
    let mut url = cfg.server.get_api_url();
    if !url.ends_with('/') { url.push('/'); }
    url.push_str("v1/");
    url.push_str(if route.starts_with('/') { &route[1..] } else { route });
    url
}

/// Initialize a new ureq agent using the given configuration.
pub fn init_agent(cfg: &Config) -> Agent {
    AgentBuilder::new()
        .timeout_connect(Duration::from_secs_f32(cfg.client.get_timeout()))
        .build()
}

#[derive(Clone)]
pub enum ClientRequestMethod {
    GET,
    POST,
    PUT,
    DELETE,
}

pub type ClientRequestHeaders = HashMap<String, String>;

/// An enum with all alternatives for the HTTP request body
#[derive(Clone)]
pub enum ClientRequestBody {
    /// No body at all, e. g. for a GET request.
    None,
    /// A JSON map, which will be serialized when the request is fired.
    Json(JsonMap),
    /// A filename. The content of the file will be streamed as request body.
    FileContent(PathBuf),
    /// All available data from STDIN will be streamed as request body.
    StdIn,
}

/// Client requests are send to the Boomack HTTP API.
///
/// This structure is rather low level and is created
/// by functions from the following modules:
///
/// * [`boomack::client::display`](../display/)
/// * [`boomack::client::eval`](../eval/)
/// * [`boomack::client::panels`](../panels/)
/// * [`boomack::client::presets`](../presets/)
/// * [`boomack::client::types`](../types/)
/// * [`boomack::client::actions`](../actions/)
#[derive(Clone)]
pub struct ClientRequest {
    /// HTTP method
    pub method: ClientRequestMethod,
    /// API version independent part of the URL path
    pub route: String,
    /// HTTP headers for the request
    pub headers: ClientRequestHeaders,
    /// Request body
    pub body: ClientRequestBody,
}

impl ClientRequest {

    pub fn new(method: ClientRequestMethod, route: String) -> ClientRequest {
        ClientRequest {
            method: method,
            route: route,
            headers: HashMap::new(),
            body: ClientRequestBody::None,
        }
    }

    pub fn set_header<K, V>(&mut self, name: K, value: V)
        where K : Into<String>, V : Into<String>
    {
        self.headers.insert(name.into(), value.into());
    }

    pub fn unset_header<K>(&mut self, name: K)
        where K: Into<String>
    {
        self.headers.remove(&name.into());
    }

    pub fn set_json_body(&mut self, data: JsonMap) {
        self.body = ClientRequestBody::Json(data);
    }

    pub fn set_file_body(&mut self, filename: PathBuf) {
        self.body = ClientRequestBody::FileContent(filename);
    }

    pub fn set_stdin_body(&mut self) {
        self.body = ClientRequestBody::StdIn;
    }

    /// Instantiates a new `ClientRequest` with the HTTP method GET and the given route.
    pub fn get(route: String) -> ClientRequest { Self::new(ClientRequestMethod::GET, route) }
    /// Instantiates a new `ClientRequest` with the HTTP method POST and the given route.
    pub fn post(route: String) -> ClientRequest { Self::new(ClientRequestMethod::POST, route) }
    /// Instantiates a new `ClientRequest` with the HTTP method PUT and the given route.
    pub fn put(route: String) -> ClientRequest { Self::new(ClientRequestMethod::PUT, route) }
    /// Instantiates a new `ClientRequest` with the HTTP method DELETE and the given route.
    pub fn delete(route: String) -> ClientRequest { Self::new(ClientRequestMethod::DELETE, route) }

}

/// Send the client request to the Boomack Server.
///
/// The URL of the server is taken from the configuration.
/// Along with parameters like timeout, etc.
pub fn send_request(cfg: &Config, client_request: &ClientRequest) -> Result<Response, Error> {
    let request_url = build_request_url(cfg, &client_request.route);

    let agent = init_agent(cfg);
    let mut req = match client_request.method {
        ClientRequestMethod::GET => agent.get(&request_url),
        ClientRequestMethod::POST => agent.post(&request_url),
        ClientRequestMethod::PUT => agent.put(&request_url),
        ClientRequestMethod::DELETE => agent.delete(&request_url),
    };
    for (name, value) in &client_request.headers {
        req = req.set(name, value);
    }

    match &client_request.body {
        ClientRequestBody::None => req.call(),
        ClientRequestBody::Json(data) => req.send_json(data),
        ClientRequestBody::FileContent(filename) => {
            let file_in = File::open(filename).expect("Could not open input file");
            req.send(file_in)
        },
        ClientRequestBody::StdIn => req.send(stdin()),
    }
}

/// Send the client request to the Boomack Server.
/// Retry if request failed.
///
/// The URL of the server is taken from the configuration.
/// Along with parameters like timeout, max retries, etc.
pub fn send_request_with_retry<FnAttempt, FnFailure, FnRetry>(
    cfg: &Config, client_request: &ClientRequest,
    mut attempt_cb: Option<FnAttempt>,
    mut failure_cb: Option<FnFailure>,
    mut retry_cb: Option<FnRetry>)
    -> Option<Response>
    where
        FnAttempt: FnMut(u32),
        FnFailure: FnMut(u32, Error),
        FnRetry: FnMut(u32)
{
    for attempt in 1..=(cfg.client.get_retry() + 1) {
        if let Some(attempt_cb) = &mut attempt_cb { attempt_cb(attempt); }
        let result = send_request(cfg, &client_request);
        match result {
            Ok(response) => {
                return Some(response);
            },
            Err(error) => {
                if let Some(failure_cb) = &mut failure_cb { failure_cb(attempt, error); }
                if matches!(client_request.body, ClientRequestBody::StdIn) {
                    // can not retry if STDIN was consumed
                    break;
                } else {
                    if attempt < cfg.client.get_retry() + 1 {
                        if let Some(retry_cb) = &mut retry_cb { retry_cb(attempt); }
                    }
                }
            }
        }
    }
    None
}