rexcli 0.18.4

Replix admin CLI tool
//
// Copyright (c) 2020 RepliXio Ltd. All rights reserved.
// Use is subject to license terms.
//

use std::borrow::Borrow;
use std::fmt;

use inspector::ResultInspector;
use serde::{de, ser};
use uuid::Uuid;

use crate::error::RexError;
use crate::latest;
use crate::output::{IntoOutput, Output};
use crate::show::Show;

pub(crate) use endpoint::ApiEndpoint;

mod endpoint;
mod realm;
mod volume;

pub(crate) type ApiResult<T> = Result<Output<T>, anyhow::Error>;

#[derive(Debug)]
pub(crate) struct Api {
    api: ApiEndpoint,
    base: String,
    token: Option<String>,
    json: bool,
    raw: bool,
    verbose: bool,
}

impl Api {
    pub(crate) fn new(
        management: &str,
        api: ApiEndpoint,
        token: &Option<String>,
        json: bool,
        raw: bool,
        verbose: bool,
    ) -> Self {
        let base = if management.starts_with("http") {
            format!("{}/{}", management, api.endpoint())
        } else if management.starts_with("api.") && management.ends_with(".replix.io") {
            format!("https://{}/{}", management, api.endpoint())
        } else {
            format!("http://{}:63214/{}", management, api.endpoint())
        };
        let token = token.clone();

        Self {
            api,
            base,
            token,
            json,
            raw,
            verbose,
        }
    }

    pub(crate) fn realm(self) -> realm::Realm {
        realm::Realm::new(self)
    }

    pub(crate) fn volume(self) -> volume::Volume {
        volume::Volume::new(self)
    }

    fn url(&self, path: impl fmt::Display) -> String {
        format!("{}{}", self.base, path)
    }

    pub(crate) fn job(
        &self,
        all: bool,
        job_id: Option<Uuid>,
        r#type: Option<&str>,
        status: &str,
    ) -> Result<String, anyhow::Error> {
        let text = if all {
            self.get_jobs().map(Show::show)
        } else if let Some(job_id) = job_id {
            self.get_job(job_id).map(Show::show)
        } else {
            self.get_jobs()
                .map(|jobs| {
                    jobs.into_iter()
                        .filter(|job| job.status.value == status)
                        .filter(|job| r#type.map(|r#type| job.r#type == r#type).unwrap_or(true))
                        .collect::<Vec<_>>()
                })
                .map(Show::show)
        }?;

        Ok(text)
    }

    pub(crate) fn version(&self, _crates: bool) -> Result<String, anyhow::Error> {
        self.get_version().map(Show::show)
    }

    fn get_job(&self, job_id: impl fmt::Display) -> ApiResult<latest::Job> {
        self.get(format!("/internal/jobs/{}", job_id))
    }

    fn get_jobs(&self) -> ApiResult<Vec<latest::Job>> {
        self.get("/internal/jobs")
    }

    fn get_version(&self) -> ApiResult<latest::InternalVersion> {
        self.get("/internal/version")
    }

    fn inspect<T>(&self, output: &Output<T>)
    where
        T: fmt::Debug,
    {
        if self.verbose {
            println!("{:#?}", output);
        }
    }

    fn del<P, I, K, V, T>(&self, path: P, params: I) -> ApiResult<T>
    where
        P: fmt::Display,
        T: de::DeserializeOwned + fmt::Debug,
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: ToString,
    {
        let url = self.url(path);
        let output = attohttpc::delete(url)
            .optionally_bearer_auth(self.token.as_ref())
            .param("include", "job")
            .params(params)
            .send()?
            .rexerror_for_status()?
            .into_output(self.raw, self.json)
            .inspect(|output| self.inspect(output))?;
        Ok(output)
    }

    fn get<P, T>(&self, path: P) -> ApiResult<T>
    where
        P: fmt::Display,
        T: de::DeserializeOwned + fmt::Debug,
    {
        let url = self.url(path);
        let output = attohttpc::get(url)
            .optionally_bearer_auth(self.token.as_ref())
            .param("include", "job")
            .send()?
            .rexerror_for_status()?
            .into_output(self.raw, self.json)
            .inspect(|output| self.inspect(output))?;

        Ok(output)
    }

    fn post<P, T, U>(&self, path: P, body: T) -> ApiResult<U>
    where
        P: fmt::Display,
        T: ser::Serialize,
        U: de::DeserializeOwned + fmt::Debug,
    {
        let url = self.url(path);
        let output = attohttpc::post(url)
            .optionally_bearer_auth(self.token.as_ref())
            .param("include", "job")
            .json(&body)?
            .send()?
            .rexerror_for_status()?
            .into_output(self.raw, self.json)
            .inspect(|output| self.inspect(output))?;

        Ok(output)
    }

    fn put<P, T>(&self, path: P) -> ApiResult<T>
    where
        P: fmt::Display,
        T: de::DeserializeOwned + fmt::Debug,
    {
        let url = format!("{}{}", self.base, path);
        let output = attohttpc::put(url)
            .optionally_bearer_auth(self.token.as_ref())
            .param("include", "job")
            .send()?
            .rexerror_for_status()?
            .into_output(self.raw, self.json)
            .inspect(|output| self.inspect(output))?;
        Ok(output)
    }
}

trait Optionally {
    fn optionally_bearer_auth(self, token: Option<impl Into<String>>) -> Self;

    fn optionally<T, F>(self, option: Option<T>, f: F) -> Self
    where
        F: FnOnce(Self, T) -> Self,
        Self: Sized,
    {
        if let Some(option) = option {
            f(self, option)
        } else {
            self
        }
    }
}

impl Optionally for attohttpc::RequestBuilder {
    fn optionally_bearer_auth(self, token: Option<impl Into<String>>) -> Self {
        self.optionally(token, |this, token| this.bearer_auth(token))
    }
}