use anyhow::{Context, Result, bail, ensure};
use std::{collections::HashSet, error::Error as StdError, fmt, sync::Arc};
use crate::ast::{NetsukeManifest, Target};
use crate::cli::Cli;
use crate::ir::BuildGraph;
use crate::localization::{self, keys};
use crate::status::PipelineStage;
use super::super::RunnerError;
use super::super::path_helpers::{ensure_manifest_exists, resolve_manifest_path};
use super::terminal_safe;
pub(super) struct HelpEntry {
pub(super) name: String,
pub(super) description: Option<Arc<str>>,
pub(super) is_action: bool,
pub(super) is_default: bool,
}
pub(super) struct HelpTargetsQuery {
pub(super) entries: Vec<HelpEntry>,
pub(super) stages: Vec<PipelineStage>,
}
#[derive(Debug)]
pub(super) struct HelpTargetsQueryFailure {
pub(super) error: anyhow::Error,
pub(super) stages: Vec<PipelineStage>,
}
impl fmt::Display for HelpTargetsQueryFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(formatter)
}
}
impl StdError for HelpTargetsQueryFailure {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.error.as_ref())
}
}
pub(super) fn query_help_targets(
cli: &Cli,
) -> std::result::Result<HelpTargetsQuery, HelpTargetsQueryFailure> {
let mut stages = Vec::new();
let result = query_entries(cli, &mut stages);
match result {
Ok(entries) => Ok(HelpTargetsQuery { entries, stages }),
Err(error) => Err(HelpTargetsQueryFailure { error, stages }),
}
}
fn query_entries(cli: &Cli, stages: &mut Vec<PipelineStage>) -> Result<Vec<HelpEntry>> {
let manifest_path = resolve_manifest_path(cli)?;
if let Err(error) = ensure_manifest_exists(cli, &manifest_path) {
record_missing_manifest_stage(&error, stages);
return Err(error);
}
let manifest = load_manifest_for_query(&manifest_path, stages)?;
reject_terminal_controls_in_target_names(&manifest)?;
BuildGraph::from_manifest(&manifest)
.context(localization::message(keys::RUNNER_CONTEXT_BUILD_GRAPH))?;
let entries = build_catalogue(&manifest);
validate_defaults(&manifest.defaults, &entries)?;
Ok(entries)
}
fn reject_terminal_controls_in_target_names(manifest: &NetsukeManifest) -> Result<()> {
let targets = manifest.actions.iter().chain(&manifest.targets);
if targets
.flat_map(|target| target.name.to_string_vec())
.any(|name| name.chars().any(super::is_terminal_control))
{
bail!("help targets cannot validate a target name with terminal control characters");
}
Ok(())
}
fn record_missing_manifest_stage(error: &anyhow::Error, stages: &mut Vec<PipelineStage>) {
if error
.downcast_ref::<RunnerError>()
.is_some_and(|runner_error| matches!(runner_error, RunnerError::ManifestNotFound { .. }))
{
stages.push(PipelineStage::ManifestIngestion);
}
}
pub(super) fn build_catalogue(manifest: &NetsukeManifest) -> Vec<HelpEntry> {
let mut entries = Vec::new();
let defaults: HashSet<&str> = manifest.defaults.iter().map(String::as_str).collect();
for target in &manifest.actions {
append_target_entries(&mut entries, target, true, &defaults);
}
for target in &manifest.targets {
append_target_entries(&mut entries, target, false, &defaults);
}
entries
}
fn validate_defaults(defaults: &[String], entries: &[HelpEntry]) -> Result<()> {
let names: HashSet<&str> = entries.iter().map(|entry| entry.name.as_str()).collect();
for default in defaults {
let safe_default = terminal_safe(default);
ensure!(
names.contains(default.as_str()),
localization::message(keys::RUNNER_MANIFEST_DEFAULT_NOT_DECLARED)
.with_arg("default", safe_default.as_ref())
);
}
Ok(())
}
fn append_target_entries(
entries: &mut Vec<HelpEntry>,
target: &Target,
is_action: bool,
defaults: &HashSet<&str>,
) {
let description = target.description.as_deref().map(Arc::<str>::from);
for name in target.name.to_string_vec() {
entries.push(HelpEntry {
is_default: defaults.contains(name.as_str()),
name,
description: description.clone(),
is_action,
});
}
}
fn load_manifest_for_query(
manifest_path: &camino::Utf8PathBuf,
stages: &mut Vec<PipelineStage>,
) -> Result<NetsukeManifest> {
let mut on_stage = |stage| stages.push(pipeline_stage(stage));
crate::manifest::from_path_for_manifest_query(manifest_path.as_std_path(), Some(&mut on_stage))
.with_context(|| {
localization::message(keys::RUNNER_CONTEXT_LOAD_MANIFEST)
.with_arg("path", manifest_path.as_str())
})
}
const fn pipeline_stage(stage: crate::manifest::ManifestLoadStage) -> PipelineStage {
match stage {
crate::manifest::ManifestLoadStage::ManifestIngestion => PipelineStage::ManifestIngestion,
crate::manifest::ManifestLoadStage::InitialYamlParsing => PipelineStage::InitialYamlParsing,
crate::manifest::ManifestLoadStage::TemplateExpansion => PipelineStage::TemplateExpansion,
crate::manifest::ManifestLoadStage::FinalRendering => PipelineStage::FinalRendering,
}
}