rototo 0.1.0-alpha.8

Control plane for runtime configuration of your application.
Documentation
use super::*;

use super::lint::{
    compact_json_option, diagnostic_location_label_for_location, semantic_target_label,
};

pub(crate) fn print_package_diff(diff: &PackageDiff, json: bool) -> Result<()> {
    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(diff).map_err(|err| RototoError::new(err.to_string()))?
        );
        return Ok(());
    }

    println!("{} {}", style::label("before"), style::bold(&diff.before));
    println!("{} {}", style::label("after"), style::bold(&diff.after));
    if diff.changes.is_empty() {
        println!(
            "{} {}",
            style::label("semantic changes"),
            style::dim("none")
        );
    } else {
        println!("{}", style::label("semantic changes"));
        for change in &diff.changes {
            print_semantic_change_header(change);
            if let Some(location) = &change.before_location {
                println!(
                    "    {} {}",
                    style::dim("before location:"),
                    style::info(&diagnostic_location_label_for_location(location))
                );
            }
            if let Some(location) = &change.after_location {
                println!(
                    "    {} {}",
                    style::dim("after location:"),
                    style::info(&diagnostic_location_label_for_location(location))
                );
            }
            if change.before.is_some() || change.after.is_some() {
                print_diff_value_line("before", &change.before, DiffSide::Before)?;
                print_diff_value_line("after", &change.after, DiffSide::After)?;
            }
            if let Some(detail) = &change.detail {
                println!(
                    "    {} {}",
                    style::dim("impact:"),
                    style::info(&compact_json(detail)?)
                );
            }
        }
    }
    if !diff.resolution_impacts.is_empty() {
        println!("{}", style::label("resolution impact"));
        for impact in &diff.resolution_impacts {
            println!(
                "  {} {}",
                style::dim("variable:"),
                style::sea(&impact.variable)
            );
            print_resolution_impact_line("before", &impact.before, DiffSide::Before)?;
            print_resolution_impact_line("after", &impact.after, DiffSide::After)?;
        }
    }
    Ok(())
}

#[derive(Clone, Copy)]
pub(super) enum DiffSide {
    Before,
    After,
}

pub(super) fn print_semantic_change_header(change: &rototo::model::SemanticChange) {
    let target = semantic_target_label(&change.target);
    let description = semantic_change_description(&change.kind);
    if !style::enabled() {
        println!("  {}  {}", change.kind, target);
        println!("    change: {description}");
        return;
    }

    let marker = semantic_change_marker(change);
    println!(
        "  {} {}  {}",
        style_diff_marker(marker),
        style_diff_kind(description, marker),
        style::sea(&target)
    );
    println!("    {} {}", style::dim("kind:"), style::dim(&change.kind));
}

pub(super) fn semantic_change_marker(change: &rototo::model::SemanticChange) -> DiffChangeMarker {
    if change.kind.ends_with("_added") || (change.before.is_none() && change.after.is_some()) {
        DiffChangeMarker::Added
    } else if change.kind.ends_with("_removed")
        || (change.before.is_some() && change.after.is_none())
    {
        DiffChangeMarker::Removed
    } else {
        DiffChangeMarker::Changed
    }
}

#[derive(Clone, Copy)]
pub(super) enum DiffChangeMarker {
    Added,
    Removed,
    Changed,
}

pub(super) fn style_diff_marker(marker: DiffChangeMarker) -> String {
    match marker {
        DiffChangeMarker::Added => style::ok("+"),
        DiffChangeMarker::Removed => style::err("−"),
        DiffChangeMarker::Changed => style::warn("~"),
    }
}

pub(super) fn style_diff_kind(kind: &str, marker: DiffChangeMarker) -> String {
    match marker {
        DiffChangeMarker::Added => style::ok(kind),
        DiffChangeMarker::Removed => style::err(kind),
        DiffChangeMarker::Changed => style::warn(kind),
    }
}

pub(super) fn semantic_change_description(kind: &str) -> &'static str {
    match kind {
        "variable_added" => "variable added",
        "variable_removed" => "variable removed",
        "variable_type_changed" => "variable type changed",
        "variable_value_added" => "variable value added",
        "variable_value_removed" => "variable value removed",
        "variable_value_changed" => "variable value changed",
        "variable_resolve_default_changed" => "variable resolve default changed",
        "variable_rule_added" => "variable resolve rule added",
        "variable_rule_removed" => "variable resolve rule removed",
        "variable_rule_when_changed" => "variable rule condition changed",
        "variable_rule_value_changed" => "variable rule value changed",
        "catalog_added" => "catalog added",
        "catalog_removed" => "catalog removed",
        "catalog_schema_changed" => "catalog schema changed",
        "variable_resolution_changed" => "variable resolution changed",
        "layer_added" => "layer added",
        "layer_removed" => "layer removed",
        "layer_diversion_changed" => "layer diversion changed",
        "allocation_added" => "allocation added",
        "allocation_removed" => "allocation removed",
        "allocation_status_changed" => "allocation status changed",
        "allocation_eligibility_changed" => "allocation eligibility changed",
        "allocation_arms_expanded" => "allocation arms expanded into unclaimed buckets",
        "allocation_arms_reassigned" => "allocation arms reassigned claimed buckets",
        "catalog_entry_added" => "catalog value added",
        "catalog_entry_removed" => "catalog value removed",
        "catalog_entry_changed" => "catalog value changed",
        "list_added" => "list added",
        "list_removed" => "list removed",
        "list_changed" => "list changed",
        "evaluation_context_added" => "evaluation context added",
        "evaluation_context_removed" => "evaluation context removed",
        "evaluation_context_schema_changed" => "evaluation context schema changed",
        "sample_added" => "sample added",
        "sample_removed" => "sample removed",
        "sample_changed" => "sample changed",
        _ => "semantic change",
    }
}

pub(super) fn print_diff_value_line(
    label: &str,
    value: &Option<serde_json::Value>,
    side: DiffSide,
) -> Result<()> {
    let value = compact_json_option(value)?;
    if !style::enabled() {
        println!("    {label}: {value}");
        return Ok(());
    }

    println!(
        "    {} {}",
        style_diff_side_label(label, side),
        style_diff_side_value(&value, side)
    );
    Ok(())
}

pub(super) fn print_resolution_impact_line(
    label: &str,
    resolution: &rototo::model::VariableResolution,
    side: DiffSide,
) -> Result<()> {
    let source = resolution_source_label(&resolution.source);
    let value = compact_json(&resolution.value)?;
    if !style::enabled() {
        println!("    {label}: {source} {value}");
        return Ok(());
    }

    println!(
        "    {} {} {}",
        style_diff_side_label(label, side),
        style::dim(&source),
        style_diff_side_value(&value, side)
    );
    Ok(())
}

pub(super) fn style_diff_side_label(label: &str, side: DiffSide) -> String {
    let label = format!("{label}:");
    match side {
        DiffSide::Before => style::err(&label),
        DiffSide::After => style::ok(&label),
    }
}

pub(super) fn style_diff_side_value(value: &str, side: DiffSide) -> String {
    if value == "<none>" {
        return style::dim(value);
    }
    match side {
        DiffSide::Before => style::err(value),
        DiffSide::After => style::ok(value),
    }
}

pub(super) fn resolution_source_label(source: &rototo::model::VariableResolutionSource) -> String {
    match source {
        rototo::model::VariableResolutionSource::Literal => "literal".to_owned(),
        rototo::model::VariableResolutionSource::Catalog { catalog, value } => {
            format!("{catalog}:{value}")
        }
        rototo::model::VariableResolutionSource::CatalogArray { catalog, values } => {
            format!("{catalog}:[{}]", values.join(","))
        }
    }
}