alp-cli 0.1.6

The native `alp` CLI for ALP SDK embedded projects: board.yaml validate/generate, project scaffolding, toolchain bootstrap, and west build/flash with a stable JSON envelope.
// SPDX-License-Identifier: Apache-2.0
//! `alp explain` — describe project/module templates and generation targets.
//!
//! Mirrors TS `runExplainCommand`: `--template` explains an init/scaffold
//! template, `--target` explains a generation output target, and no selector
//! prints an overview. Supplying both is an error (exit 1), as is an unknown id.

use alp_core::wizard::{ModuleTemplateDefinition, WizardFeatureFlags, WizardTemplateDefinition};
use alp_core::wizard::{list_module_templates, list_wizard_templates};
use alp_core::{GenerationTargetSupport, list_generation_target_support};

use super::CommandRun;
use crate::cli::{ExplainArgs, GlobalArgs};
use crate::envelope::{Envelope, Issue, Project};
use crate::exit::ExitCode;

/// Identifies what the explain result describes: its `kind`
/// (`overview` / `project-template` / `module-template` / `generation-target`)
/// and the resolved `value` (e.g. the template/target id).
#[derive(serde::Serialize)]
struct Selector {
    /// Category of the explained topic.
    kind: String,
    /// Resolved id of the explained topic (empty for an overview).
    value: String,
}

/// The catalog of all explainable ids, always emitted so callers can discover
/// valid selectors even on a failure.
#[derive(serde::Serialize)]
struct Available {
    /// `alp init` project template ids.
    #[serde(rename = "projectTemplates")]
    project_templates: Vec<String>,
    /// `alp scaffold` module template ids.
    #[serde(rename = "moduleTemplates")]
    module_templates: Vec<String>,
    /// Generation output target ids (the `emit` keys).
    #[serde(rename = "generationTargets")]
    generation_targets: Vec<String>,
}

/// JSON `data` payload of the `explain` envelope: the resolved selector, a
/// one-line summary, free-form detail lines, and the `available` catalog.
#[derive(serde::Serialize)]
struct ExplainData {
    /// Payload schema version (currently `"1"`).
    #[serde(rename = "schemaVersion")]
    schema_version: String,
    /// What this result describes.
    selector: Selector,
    /// Human-readable one-line summary.
    summary: String,
    /// Detail lines elaborating the summary.
    details: Vec<String>,
    /// Catalog of all explainable ids.
    available: Available,
}

/// Runs `alp explain`: resolves `--template`/`--target` (mutually exclusive)
/// against the wizard/module/generation catalogs, or prints an overview when
/// neither is given. Both selectors or an unknown id yield a runtime failure.
pub fn run(g: &GlobalArgs, args: &ExplainArgs) -> CommandRun {
    let project_templates = list_wizard_templates();
    let module_templates = list_module_templates();
    let generation_targets = list_generation_target_support();

    let available = || Available {
        project_templates: project_templates
            .iter()
            .map(|t| t.id.as_str().to_string())
            .collect(),
        module_templates: module_templates
            .iter()
            .map(|t| t.id.as_str().to_string())
            .collect(),
        generation_targets: generation_targets
            .iter()
            .map(|t| t.emit.to_string())
            .collect(),
    };

    let requested_template = args
        .template
        .as_deref()
        .map(str::trim)
        .filter(|s| !s.is_empty());
    let requested_target = g.target.as_deref().map(str::trim).filter(|s| !s.is_empty());

    if requested_template.is_some() && requested_target.is_some() {
        return failure(
            g,
            available(),
            Selector {
                kind: "overview".to_string(),
                value: String::new(),
            },
            "ambiguous-selector",
            "Use either --template or --target for explain, not both.",
            vec![
                "explain: use either --template or --target, but not both in the same command."
                    .to_string(),
            ],
        );
    }

    if let Some(requested) = requested_template {
        if let Some(pt) = project_templates
            .iter()
            .copied()
            .find(|t| t.id.as_str() == requested)
        {
            return success(
                g,
                available(),
                Selector {
                    kind: "project-template".to_string(),
                    value: pt.id.as_str().to_string(),
                },
                format!("{} ({})", pt.label, pt.id.as_str()),
                project_template_details(pt),
            );
        }
        if let Some(mt) = module_templates
            .iter()
            .copied()
            .find(|t| t.id.as_str() == requested)
        {
            return success(
                g,
                available(),
                Selector {
                    kind: "module-template".to_string(),
                    value: mt.id.as_str().to_string(),
                },
                format!("{} ({})", mt.label, mt.id.as_str()),
                module_template_details(mt),
            );
        }
        return failure(
            g,
            available(),
            Selector {
                kind: "overview".to_string(),
                value: requested.to_string(),
            },
            "template-unknown",
            &format!("Unknown template '{requested}'."),
            vec![format!(
                "explain: unknown template '{requested}'. Run alp explain without selectors to list available topics."
            )],
        );
    }

    if let Some(requested) = requested_target {
        let Some(target) = generation_targets.iter().find(|t| t.emit == requested) else {
            return failure(
                g,
                available(),
                Selector {
                    kind: "overview".to_string(),
                    value: requested.to_string(),
                },
                "target-unknown",
                &format!("Unknown generation target '{requested}'."),
                vec![format!(
                    "explain: unknown generation target '{requested}'. Run alp explain without selectors to list available targets."
                )],
            );
        };
        return success(
            g,
            available(),
            Selector {
                kind: "generation-target".to_string(),
                value: target.emit.to_string(),
            },
            format!("{} ({})", target.display_name, target.emit),
            generation_target_details(target),
        );
    }

    let overview = vec![
        "Use --template to explain a project template (init) or module template (scaffold)."
            .to_string(),
        "Use --target to explain a generation output target.".to_string(),
        format!(
            "Project templates: {}",
            project_templates
                .iter()
                .map(|t| t.id.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ),
        format!(
            "Module templates: {}",
            module_templates
                .iter()
                .map(|t| t.id.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ),
        format!(
            "Generation targets: {}",
            generation_targets
                .iter()
                .map(|t| t.emit)
                .collect::<Vec<_>>()
                .join(", ")
        ),
    ];
    success(
        g,
        available(),
        Selector {
            kind: "overview".to_string(),
            value: "all".to_string(),
        },
        "ALP explain topics".to_string(),
        overview,
    )
}

/// Builds the detail lines for a project (`alp init`) template: description,
/// per-template explanation, default libraries, and default feature flags.
fn project_template_details(pt: &WizardTemplateDefinition) -> Vec<String> {
    let mut details = vec![pt.description.to_string()];
    details.extend(pt.explanation.iter().map(|s| (*s).to_string()));
    let libraries = if pt.libs.is_empty() {
        "(none)".to_string()
    } else {
        pt.libs.join(", ")
    };
    details.push(format!("Default libraries: {libraries}"));
    details.push(format!(
        "Default features: {}",
        format_feature_flags(pt.features.as_ref())
    ));
    details
}

/// Builds the detail lines for a module (`alp scaffold`) template: description,
/// generated function prefix, and a usage hint.
fn module_template_details(mt: &ModuleTemplateDefinition) -> Vec<String> {
    vec![
        mt.description.to_string(),
        format!("Function prefix: {}", mt.function_prefix),
        "Use this template with alp scaffold to generate a module source/header baseline."
            .to_string(),
    ]
}

/// Builds the detail lines for a generation target: display name, output path,
/// and preview label/language.
fn generation_target_details(target: &GenerationTargetSupport) -> Vec<String> {
    vec![
        format!("Display name: {}", target.display_name),
        format!("Output path: {}", target.output_relative_path),
        format!("Preview label: {}", target.preview_label),
        format!("Preview language: {}", target.preview_language_id),
    ]
}

/// Formats the four wizard feature flags as `wifi=.. mqtt=.. ble=.. tls=..`,
/// treating a `None` flag set as all-false.
fn format_feature_flags(features: Option<&WizardFeatureFlags>) -> String {
    let (wifi, mqtt, ble, tls) = match features {
        Some(f) => (f.wifi, f.mqtt, f.ble, f.tls),
        None => (false, false, false, false),
    };
    format!("wifi={wifi} mqtt={mqtt} ble={ble} tls={tls}")
}

/// An empty `Project` (no root, no `board.yaml`) — explain is project-agnostic.
fn null_project() -> Project {
    Project {
        root: None,
        board_yaml: None,
    }
}

/// Assembles a success `CommandRun` (exit `Success`): text lines in human mode,
/// or an `explain` envelope with `ExplainData` in JSON mode.
fn success(
    g: &GlobalArgs,
    available: Available,
    selector: Selector,
    summary: String,
    details: Vec<String>,
) -> CommandRun {
    let text = if g.is_json() {
        Vec::new()
    } else {
        explain_text(&summary, &details, g)
    };
    let data = ExplainData {
        schema_version: "1".to_string(),
        selector,
        summary,
        details,
        available,
    };
    let json = g.is_json().then(|| {
        Envelope::new(
            "explain",
            null_project(),
            data,
            Vec::new(),
            ExitCode::Success.code(),
        )
        .to_json()
    });
    CommandRun {
        exit: ExitCode::Success,
        text,
        json,
    }
}

/// Assembles a failure `CommandRun` (exit `RuntimeFailure`) carrying an
/// `explain.{code}` error issue: the given `text_lines` in human mode, or an
/// envelope with empty summary/details in JSON mode.
fn failure(
    g: &GlobalArgs,
    available: Available,
    selector: Selector,
    code: &str,
    issue_message: &str,
    text_lines: Vec<String>,
) -> CommandRun {
    let issues = vec![Issue {
        code: format!("explain.{code}"),
        severity: "error".to_string(),
        message: issue_message.to_string(),
    }];
    let data = ExplainData {
        schema_version: "1".to_string(),
        selector,
        summary: String::new(),
        details: Vec::new(),
        available,
    };
    let text = if g.is_json() { Vec::new() } else { text_lines };
    let json = g.is_json().then(|| {
        Envelope::new(
            "explain",
            null_project(),
            data,
            issues,
            ExitCode::RuntimeFailure.code(),
        )
        .to_json()
    });
    CommandRun {
        exit: ExitCode::RuntimeFailure,
        text,
        json,
    }
}

/// Renders human-mode output: an `explain: {summary}` header followed by one
/// `- {detail}` line per detail (detail lines are suppressed when `--quiet`).
fn explain_text(summary: &str, details: &[String], g: &GlobalArgs) -> Vec<String> {
    let mut lines = vec![format!("explain: {summary}")];
    if !g.quiet {
        for detail in details {
            lines.push(format!("- {detail}"));
        }
    }
    lines
}