cloudcover-site-data 0.1.2

Static site data generator for CloudCover
use std::{error::Error, fs, path::Path};

use cloudcover_site_data::generate;
use serde_json::Value;

fn field<'a>(value: &'a Value, name: &str) -> Result<&'a Value, Box<dyn Error>> {
    value
        .get(name)
        .ok_or_else(|| format!("missing JSON field {name:?}").into())
}

fn array_item(value: &Value, index: usize) -> Result<&Value, Box<dyn Error>> {
    value
        .get(index)
        .ok_or_else(|| format!("missing JSON array item {index}").into())
}

fn string_value(value: &Value, context: &str) -> Result<String, Box<dyn Error>> {
    value
        .as_str()
        .map(str::to_owned)
        .ok_or_else(|| format!("{context} is not a string").into())
}

fn usize_value(value: &Value, context: &str) -> Result<usize, Box<dyn Error>> {
    let number = value
        .as_u64()
        .ok_or_else(|| format!("{context} is not an unsigned integer"))?;
    usize::try_from(number).map_err(|_| format!("{context} does not fit usize").into())
}

fn read_json(path: &Path) -> Result<Value, Box<dyn Error>> {
    Ok(serde_json::from_slice(&fs::read(path)?)?)
}

#[test]
#[allow(clippy::too_many_lines)]
fn generated_catalogs_are_consistent() -> Result<(), Box<dyn Error>> {
    let temporary_directory = tempfile::tempdir()?;
    let output = temporary_directory.path().join("data");
    generate(&output)?;

    let api = read_json(&output.join("api.json"))?;
    assert_eq!(field(&api, "schema_version")?.as_u64(), Some(1));
    let iam_values = field(&api, "iam_permissions")?
        .as_array()
        .ok_or("iam_permissions is not an array")?;
    let iam_permissions = iam_values
        .iter()
        .enumerate()
        .map(|(index, value)| string_value(value, &format!("IAM permission {index}")))
        .collect::<Result<Vec<_>, _>>()?;
    assert!(
        iam_permissions
            .windows(2)
            .all(|window| window[0] < window[1])
    );

    let api_values = field(&api, "api_methods")?
        .as_array()
        .ok_or("api_methods is not an array")?;
    let mut api_rows = Vec::with_capacity(api_values.len());
    for (index, value) in api_values.iter().enumerate() {
        let row = value.as_array().ok_or("API method row is not an array")?;
        assert_eq!(row.len(), 3, "API method row {index} has the wrong shape");
        let service = string_value(array_item(value, 0)?, "API service")?;
        let name = string_value(array_item(value, 1)?, "API operation")?;
        let permission_ids = array_item(value, 2)?
            .as_array()
            .ok_or("API permission IDs are not an array")?
            .iter()
            .map(|value| {
                let permission_id = usize_value(value, "API permission ID")?;
                if permission_id >= iam_permissions.len() {
                    return Err(format!("API permission ID {permission_id} is dangling").into());
                }
                Ok(permission_id)
            })
            .collect::<Result<Vec<_>, Box<dyn Error>>>()?;
        assert!(
            permission_ids
                .windows(2)
                .all(|window| window[0] < window[1])
        );
        api_rows.push((service, name, permission_ids));
    }
    assert!(api_rows.windows(2).all(|window| {
        (window[0].0.as_str(), window[0].1.as_str()) < (window[1].0.as_str(), window[1].1.as_str())
    }));

    let api_create_bucket_id = api_rows
        .iter()
        .position(|row| row.0 == "s3" && row.1 == "CreateBucket")
        .ok_or("missing s3:CreateBucket API method")?;
    let iam_create_bucket_id = iam_permissions
        .iter()
        .position(|permission| permission == "s3:CreateBucket")
        .ok_or("missing s3:CreateBucket IAM permission")?;
    assert!(
        api_rows[api_create_bucket_id]
            .2
            .contains(&iam_create_bucket_id)
    );

    let terraform_index = read_json(&output.join("terraform/index.json"))?;
    assert_eq!(field(&terraform_index, "schema_version")?.as_u64(), Some(1));
    let versions = field(&terraform_index, "versions")?
        .as_array()
        .ok_or("Terraform versions is not an array")?
        .iter()
        .enumerate()
        .map(|(index, value)| string_value(value, &format!("Terraform version {index}")))
        .collect::<Result<Vec<_>, _>>()?;
    assert!(!versions.is_empty());
    assert!(versions.windows(2).all(|window| window[0] != window[1]));
    let latest = string_value(field(&terraform_index, "latest")?, "Terraform latest")?;
    assert_eq!(versions.last(), Some(&latest));

    let terraform_rows_document = read_json(&output.join("terraform/rows.json"))?;
    assert_eq!(
        field(&terraform_rows_document, "schema_version")?.as_u64(),
        Some(1)
    );
    let row_values = field(&terraform_rows_document, "rows")?
        .as_array()
        .ok_or("Terraform rows is not an array")?;
    let mut terraform_rows = Vec::with_capacity(row_values.len());
    for value in row_values {
        let row = value.as_array().ok_or("Terraform row is not an array")?;
        assert_eq!(row.len(), 3, "Terraform row has the wrong shape");
        let resource = string_value(array_item(value, 0)?, "Terraform resource")?;
        let lifecycle = string_value(array_item(value, 1)?, "Terraform lifecycle")?;
        let api_ids = array_item(value, 2)?
            .as_array()
            .ok_or("Terraform API IDs are not an array")?
            .iter()
            .map(|value| {
                let api_id = usize_value(value, "Terraform API ID")?;
                if api_id >= api_rows.len() {
                    return Err(format!("Terraform API ID {api_id} is dangling").into());
                }
                Ok(api_id)
            })
            .collect::<Result<Vec<_>, Box<dyn Error>>>()?;
        assert!(api_ids.windows(2).all(|window| window[0] < window[1]));
        terraform_rows.push((resource, lifecycle, api_ids));
    }
    assert!(
        terraform_rows
            .windows(2)
            .all(|window| window[0] < window[1])
    );

    for version in &versions {
        let snapshot = read_json(&output.join(format!("terraform/versions/{version}.json")))?;
        assert_eq!(field(&snapshot, "schema_version")?.as_u64(), Some(1));
        assert_eq!(
            string_value(field(&snapshot, "version")?, "snapshot version")?,
            *version
        );
        let row_ids = field(&snapshot, "row_ids")?
            .as_array()
            .ok_or("snapshot row IDs are not an array")?
            .iter()
            .map(|value| {
                let row_id = usize_value(value, "snapshot row ID")?;
                if row_id >= terraform_rows.len() {
                    return Err(format!("snapshot row ID {row_id} is dangling").into());
                }
                Ok(row_id)
            })
            .collect::<Result<Vec<_>, Box<dyn Error>>>()?;
        assert!(row_ids.windows(2).all(|window| window[0] < window[1]));
    }

    let snapshot = read_json(&output.join("terraform/versions/6.64.0.json"))?;
    let snapshot_row_ids = field(&snapshot, "row_ids")?
        .as_array()
        .ok_or("6.64.0 row IDs are not an array")?;
    let bucket_create = snapshot_row_ids
        .iter()
        .map(|value| usize_value(value, "6.64.0 row ID"))
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .map(|row_id| &terraform_rows[row_id])
        .find(|row| row.0 == "aws_s3_bucket" && row.1 == "create")
        .ok_or("missing aws_s3_bucket create row in 6.64.0")?;
    assert!(bucket_create.2.contains(&api_create_bucket_id));
    assert!(
        api_rows[api_create_bucket_id]
            .2
            .contains(&iam_create_bucket_id)
    );

    Ok(())
}