fundaia 0.55.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use super::*;

pub struct NewEnvironment<'a> {
    pub name: &'a str,
    pub copy_from: Option<&'a str>,
    pub empty: bool,
    pub deploy: bool,
    pub ttl: Option<&'a str>,
}

impl ApiClient {
    pub async fn environments(&self, project_id: &str) -> Result<Answer<EnvironmentsPage>> {
        self.get_answer(&format!("/api/infra/projects/{project_id}/environments"))
            .await
    }

    pub async fn create_environment(
        &self,
        project_id: &str,
        draft: &NewEnvironment<'_>,
    ) -> Result<Answer<CreatedEnvironment>> {
        self.post_answer(
            &format!("/api/infra/projects/{project_id}/environments"),
            &environment_body(draft),
        )
        .await
    }

    pub async fn rename_environment(
        &self,
        project_id: &str,
        environment: &str,
        name: &str,
    ) -> Result<Answer<EnvironmentPage>> {
        self.answer(
            Method::PATCH,
            &format!(
                "/api/infra/projects/{project_id}/environments/{}",
                utf8_percent_encode(environment, NON_ALPHANUMERIC)
            ),
            Some(&serde_json::json!({ "name": name })),
        )
        .await
    }

    pub async fn delete_environment(
        &self,
        project_id: &str,
        environment: &str,
        confirmation: &str,
    ) -> Result<()> {
        self.delete(&format!(
            "/api/infra/projects/{project_id}/environments/{}?confirm={}",
            utf8_percent_encode(environment, NON_ALPHANUMERIC),
            utf8_percent_encode(confirmation, NON_ALPHANUMERIC)
        ))
        .await
    }

    pub async fn set_environment_expiry(
        &self,
        project_id: &str,
        environment: &str,
        ttl: Option<&str>,
    ) -> Result<Answer<EnvironmentPage>> {
        self.answer(
            Method::PATCH,
            &format!(
                "/api/infra/projects/{project_id}/environments/{}",
                utf8_percent_encode(environment, NON_ALPHANUMERIC)
            ),
            Some(&serde_json::json!({ "ttl": ttl })),
        )
        .await
    }

    pub async fn compare_environments(
        &self,
        project_id: &str,
        from: &str,
        to: &str,
    ) -> Result<Answer<EnvironmentComparison>> {
        self.get_answer(&format!(
            "/api/infra/projects/{project_id}/environments/compare?from={}&to={}",
            utf8_percent_encode(from, NON_ALPHANUMERIC),
            utf8_percent_encode(to, NON_ALPHANUMERIC)
        ))
        .await
    }

    pub async fn promotion_plan(
        &self,
        project_id: &str,
        from: &str,
        to: &str,
        services: &[String],
    ) -> Result<Answer<PromotionPlan>> {
        self.get_answer(&format!(
            "{}/promote?to={}{}",
            environment_path(project_id, from),
            utf8_percent_encode(to, NON_ALPHANUMERIC),
            services_query(services)
        ))
        .await
    }

    pub async fn promote_environment(
        &self,
        project_id: &str,
        from: &str,
        to: &str,
        services: &[String],
    ) -> Result<Answer<PromotionPlan>> {
        self.post_answer(
            &self.released(&format!("{}/promote", environment_path(project_id, from))),
            &serde_json::json!({ "to": to, "services": services }),
        )
        .await
    }

    pub async fn data_copy_plan(
        &self,
        project_id: &str,
        from: &str,
        to: &str,
        services: &[String],
    ) -> Result<Answer<DataCopyPlan>> {
        self.get_answer(&format!(
            "{}/copy-data?to={}{}",
            environment_path(project_id, from),
            utf8_percent_encode(to, NON_ALPHANUMERIC),
            services_query(services)
        ))
        .await
    }

    pub async fn copy_environment_data(
        &self,
        project_id: &str,
        from: &str,
        to: &str,
        services: &[String],
        confirmation: &str,
    ) -> Result<Answer<DataCopyPlan>> {
        let path = format!("{}/copy-data", environment_path(project_id, from));
        let response = self
            .unbounded_request(
                Method::POST,
                &path,
                Some(
                    &serde_json::json!({ "to": to, "services": services, "confirm": confirmation }),
                ),
            )
            .send()
            .await?;
        let text = Self::check(response).await?;
        let raw = serde_json::from_str(&text)
            .with_context(|| format!("could not read the answer from {path}"))?;
        decoded(raw, &path)
    }

    pub async fn copy_service_to_environment(
        &self,
        project_id: &str,
        environment: &str,
        from: &str,
        deploy: bool,
    ) -> Result<Answer<CopiedServicePage>> {
        let mut body = serde_json::Map::new();
        body.insert("from".into(), from.into());
        if !deploy {
            body.insert("deploy".into(), false.into());
        }

        self.post_answer(
            &format!(
                "/api/infra/projects/{project_id}/environments/{}/services",
                utf8_percent_encode(environment, NON_ALPHANUMERIC)
            ),
            &serde_json::Value::Object(body),
        )
        .await
    }
}

fn environment_path(project_id: &str, environment: &str) -> String {
    format!(
        "/api/infra/projects/{project_id}/environments/{}",
        utf8_percent_encode(environment, NON_ALPHANUMERIC)
    )
}

fn services_query(services: &[String]) -> String {
    if services.is_empty() {
        return String::new();
    }
    let joined = services.join(",");
    format!(
        "&services={}",
        utf8_percent_encode(&joined, NON_ALPHANUMERIC)
    )
}

fn environment_body(draft: &NewEnvironment<'_>) -> serde_json::Value {
    let mut body = serde_json::Map::new();
    body.insert("name".into(), draft.name.into());
    if draft.empty {
        body.insert("copyFrom".into(), serde_json::Value::Null);
    } else if let Some(source) = draft.copy_from {
        body.insert("copyFrom".into(), source.into());
    }
    if !draft.deploy {
        body.insert("deploy".into(), false.into());
    }
    if let Some(ttl) = draft.ttl {
        body.insert("ttl".into(), ttl.into());
    }
    serde_json::Value::Object(body)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn named(name: &str) -> NewEnvironment<'_> {
        NewEnvironment {
            name,
            copy_from: None,
            empty: false,
            deploy: true,
            ttl: None,
        }
    }

    #[test]
    fn it_should_leave_the_source_to_the_server_when_none_is_named() {
        assert_eq!(
            environment_body(&named("staging")),
            serde_json::json!({ "name": "staging" })
        );
    }

    #[test]
    fn it_should_ask_for_an_empty_environment_with_a_null_source() {
        let draft = NewEnvironment {
            empty: true,
            ..named("sandbox")
        };
        assert_eq!(
            environment_body(&draft),
            serde_json::json!({ "name": "sandbox", "copyFrom": null })
        );
    }

    #[test]
    fn it_should_ask_for_an_environment_that_expires() {
        let draft = NewEnvironment {
            ttl: Some("7d"),
            ..named("demo")
        };
        assert_eq!(
            environment_body(&draft),
            serde_json::json!({ "name": "demo", "ttl": "7d" })
        );
    }

    #[test]
    fn it_should_ask_for_no_services_in_particular_with_no_query() {
        assert_eq!(services_query(&[]), "");
    }

    #[test]
    fn it_should_name_the_services_asked_for_in_one_query_parameter() {
        assert_eq!(
            services_query(&["api".to_owned(), "web".to_owned()]),
            "&services=api%2Cweb"
        );
    }

    #[test]
    fn it_should_say_only_no_about_deploying() {
        let draft = NewEnvironment {
            deploy: false,
            copy_from: Some("qa"),
            ..named("staging")
        };
        assert_eq!(
            environment_body(&draft),
            serde_json::json!({ "name": "staging", "copyFrom": "qa", "deploy": false })
        );
    }
}