icydb-cli 0.161.3

Developer CLI tools for IcyDB
use candid::Decode;
use icydb::db::EntitySchemaDescription;

use crate::{
    cli::CanisterTarget,
    config::{SCHEMA_ENDPOINT, require_configured_endpoint},
    icp::require_created_canister,
    table::{ColumnAlign, append_indented_table},
};

use super::{
    call_query,
    render::{render_field_list, yes_no},
};

/// Read and print the generated accepted-schema endpoint.
pub(crate) fn run_schema_show_command(target: CanisterTarget) -> Result<(), String> {
    require_configured_endpoint(target.canister_name(), SCHEMA_ENDPOINT)?;
    require_created_canister(target.environment(), target.canister_name())?;
    let candid_bytes = call_query(
        target.environment(),
        target.canister_name(),
        SCHEMA_ENDPOINT.method(),
        "()",
    )?;
    let response = Decode!(
        candid_bytes.as_slice(),
        Result<Vec<icydb::db::EntitySchemaDescription>, icydb::Error>
    )
    .map_err(|err| err.to_string())?;

    match response {
        Ok(report) => {
            print!("{}", render_schema_report(report.as_slice()));

            Ok(())
        }
        Err(err) => Err(format!(
            "IcyDB schema method '{}' failed on canister '{}' in environment '{}': {err}",
            SCHEMA_ENDPOINT.method(),
            target.canister_name(),
            target.environment(),
        )),
    }
}

pub(crate) fn render_schema_report(report: &[EntitySchemaDescription]) -> String {
    let mut output = String::new();
    let entity_rows = report
        .iter()
        .map(|entity| {
            [
                entity.entity_name().to_string(),
                entity.fields().len().to_string(),
                entity.indexes().len().to_string(),
                entity.relations().len().to_string(),
                entity.primary_key().to_string(),
                entity.entity_path().to_string(),
            ]
        })
        .collect::<Vec<_>>();
    let field_rows = report
        .iter()
        .flat_map(|entity| {
            entity.fields().iter().map(|field| {
                [
                    entity.entity_name().to_string(),
                    field.name().to_string(),
                    field
                        .slot()
                        .map_or_else(|| "-".to_string(), |slot| slot.to_string()),
                    field.kind().to_string(),
                    yes_no(field.nullable()).to_string(),
                    yes_no(field.primary_key()).to_string(),
                    yes_no(field.queryable()).to_string(),
                    field.origin().to_string(),
                ]
            })
        })
        .collect::<Vec<_>>();
    let index_rows = report
        .iter()
        .flat_map(|entity| {
            entity.indexes().iter().map(|index| {
                [
                    entity.entity_name().to_string(),
                    index.name().to_string(),
                    render_field_list(index.fields()),
                    yes_no(index.unique()).to_string(),
                    index.origin().to_string(),
                ]
            })
        })
        .collect::<Vec<_>>();
    let relation_rows = report
        .iter()
        .flat_map(|entity| {
            entity.relations().iter().map(|relation| {
                [
                    entity.entity_name().to_string(),
                    relation.field().to_string(),
                    relation.target_entity_name().to_string(),
                    format!("{:?}", relation.strength()),
                    format!("{:?}", relation.cardinality()),
                ]
            })
        })
        .collect::<Vec<_>>();

    output.push_str("IcyDB schema\n");
    output.push_str(
        format!(
            "  entities: {}\n  fields: {}\n  indexes: {}\n  relations: {}\n\n",
            report.len(),
            field_rows.len(),
            index_rows.len(),
            relation_rows.len(),
        )
        .as_str(),
    );
    append_schema_entity_table(&mut output, entity_rows.as_slice());
    output.push('\n');
    append_schema_field_table(&mut output, field_rows.as_slice());
    output.push('\n');
    append_schema_index_table(&mut output, index_rows.as_slice());
    output.push('\n');
    append_schema_relation_table(&mut output, relation_rows.as_slice());

    output
}

fn append_schema_entity_table(output: &mut String, rows: &[[String; 6]]) {
    output.push_str("entities\n");
    if rows.is_empty() {
        output.push_str("  None\n");
        return;
    }

    append_indented_table(
        output,
        "  ",
        &[
            "entity",
            "fields",
            "indexes",
            "relations",
            "primary key",
            "path",
        ],
        rows,
        &[
            ColumnAlign::Left,
            ColumnAlign::Right,
            ColumnAlign::Right,
            ColumnAlign::Right,
            ColumnAlign::Left,
            ColumnAlign::Left,
        ],
    );
}

fn append_schema_field_table(output: &mut String, rows: &[[String; 8]]) {
    output.push_str("fields\n");
    if rows.is_empty() {
        output.push_str("  None\n");
        return;
    }

    append_indented_table(
        output,
        "  ",
        &[
            "entity",
            "field",
            "slot",
            "type",
            "nullable",
            "pk",
            "queryable",
            "origin",
        ],
        rows,
        &[
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Right,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
        ],
    );
}

fn append_schema_index_table(output: &mut String, rows: &[[String; 5]]) {
    output.push_str("indexes\n");
    if rows.is_empty() {
        output.push_str("  None\n");
        return;
    }

    append_indented_table(
        output,
        "  ",
        &["entity", "index", "fields", "unique", "origin"],
        rows,
        &[
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
        ],
    );
}

fn append_schema_relation_table(output: &mut String, rows: &[[String; 5]]) {
    output.push_str("relations\n");
    if rows.is_empty() {
        output.push_str("  None\n");
        return;
    }

    append_indented_table(
        output,
        "  ",
        &["entity", "field", "target", "strength", "cardinality"],
        rows,
        &[
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
            ColumnAlign::Left,
        ],
    );
}