bular 0.0.2

CLI for managing Bular deployments
use std::collections::HashMap;

use reqwest::multipart::{Form, Part};
use serde::{Deserialize, Serialize};

pub struct Api {
    client: reqwest::Client,
    server: String,
    bearer: String,
}

impl Api {
    pub fn new(
        client: reqwest::Client,
        server: String,
        bearer: String,
    ) -> Result<Self, reqwest::Error> {
        Ok(Self {
            client,
            server,
            bearer,
        })
    }

    pub fn client(&self) -> &reqwest::Client {
        &self.client
    }

    pub fn base_url(&self) -> &str {
        &self.server
    }

    // TODO: make accessing endpoints requiring auth typesafe

    pub async fn deployment_plan(
        &self,
        app_id: String,
        assets: HashMap</* path */ String, /* hash */ String>,
    ) -> Result<Vec<String>, reqwest::Error> {
        let resp = self
            .client
            .post(&format!(
                "{}/api/application/{app_id}/deployment/plan",
                self.server
            ))
            .bearer_auth(&self.bearer)
            .json(&assets)
            .send()
            .await?;
        if !resp.status().is_success() {
            todo!(); // Error handling
        }
        Ok(resp.json().await?)
    }

    pub async fn deployment(
        &self,
        app_id: String,
        deployment: Deployment,
        mut form: Form,
    ) -> Result<DeploymentCreateResult, reqwest::Error> {
        form = form.part(
            "___manifest___",
            Part::bytes(serde_json::to_vec(&deployment).unwrap()),
        );

        let resp = self
            .client
            .post(&format!(
                "{}/api/application/{app_id}/deployment",
                self.server
            ))
            .bearer_auth(&self.bearer)
            .multipart(form)
            .send()
            .await
            .unwrap();
        if !resp.status().is_success() {
            println!("{:?} {:?}", resp.status(), resp.text().await);
            todo!(); // Error handling
        }
        Ok(resp.json().await?)
    }
}

/// The deployment manifest.
#[derive(Debug, Serialize)]
pub struct Deployment {
    pub version: u16,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub framework: Option<DeploymentFramework>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git: Option<DeploymentGit>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub routes: Option<Vec<DeploymentRoute>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub assets: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub overrides: Option<HashMap<String, DeploymentOverride>>,
}

#[derive(Debug, Serialize)]
pub struct DeploymentFramework {
    pub name: String,
    pub version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub href: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct DeploymentGit {
    pub commit: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dirty: Option<bool>,
}

#[derive(Debug, Serialize)]
pub struct DeploymentRoute {
    pub src: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub methods: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dest: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<u16>,
}

#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeploymentOverride {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_language: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_disposition: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_encoding: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cache_control: Option<String>,
    // #[serde(skip_serializing_if = "Option::is_none")]
    // cache_expiry: Option<String>, // TODO: `Date` type
}

#[derive(Debug, Deserialize)]
pub struct DeploymentCreateResult {
    pub id: String,
}

#[derive(Debug, Deserialize)]
pub struct ApiAuthCliResult {
    pub id: String,
}