sva-cli 0.14.4

The command-line front end for SVA
Documentation
// Concern: dispatches one parsed subcommand and prints its envelope | Non-concern: parsing argv (args/), the pipeline and its errors (sva-core) | IO: (argv) -> stdout JSON + an exit code

use std::process::ExitCode;

use sva_core::Diagnostic;

use sva_cli::{
    CliError, Command, Finding, Format, NAME, VERSION, analyze, builtins, builtins_data, caught,
    colored, composition, cwd, diagnostics_text, error_envelope, help_data, help_text, lint,
    lint_data, located, new_data, parse_args, render, scaffold, stopped, success_envelope, trace,
    trace_data, version_data,
};

/// Parsing, lowering and dropping a node's expression all recurse to its depth, which `sum`
/// lets an author choose. The process's own stack is fixed before it starts; a thread's is not.
const STACK_BYTES: usize = 128 << 20;

/// A lone `@ref` to a score in the current composition outlines that score as the song; any
/// other text is outlined as written, reading no composition.
fn outline(text: &str) -> Result<String, CliError> {
    let score = text
        .strip_prefix('@')
        .filter(|path| sva_ast::whole_ref_path(path))
        .filter(|path| {
            cwd().is_ok_and(|dir| {
                dir.join(format!("{path}{}", sva_ast::score::EXTENSION))
                    .is_file()
            })
        });
    let data = match score {
        Some(_) => sva_core::song_data(&sva_ast::Dir::at(composition()?), text)?,
        None => sva_core::outline_data(text)?,
    };
    Ok(success_envelope(&data, &[]))
}

fn main() -> ExitCode {
    match caught(STACK_BYTES, run) {
        Ok(code) => code,
        Err(why) => {
            println!("{}", stopped(&why));
            ExitCode::from(1)
        }
    }
}

fn run() -> ExitCode {
    let argv: Vec<String> = std::env::args().skip(1).collect();

    let parsed = parse_args(&argv);
    // The one evaluator subcommand renders its findings either way; nothing else takes `--format`.
    let format = match &parsed {
        Ok(Command::Lint { format, .. }) => *format,
        _ => Format::Json,
    };
    let outcome = match parsed {
        Ok(Command::Version) => Ok(success_envelope(&version_data(NAME, VERSION), &[])),
        Ok(Command::Help) => Ok(success_envelope(&help_data(&help_text()), &[])),
        Ok(Command::Render(args)) => render(&args),
        Ok(Command::Analyze(args)) => analyze(&args),
        Ok(Command::Lint { target, format }) => lint_composition(target.as_deref(), format),
        Ok(Command::Trace { target }) => trace_node(&target),
        Ok(Command::Builtins) => Ok(success_envelope(&builtins_data(&builtins()), &[])),
        Ok(Command::Outline { text }) => outline(&text),
        Ok(Command::New {
            name,
            idempotency_key,
        }) => scaffold_new(&name, idempotency_key.as_deref()),
        Err(e) => Err(e),
    };

    match outcome {
        Ok(json) => {
            println!("{json}");
            ExitCode::SUCCESS
        }
        Err(e) => {
            match format {
                Format::Text => {
                    let found = match e.diagnostics() {
                        held if held.is_empty() => vec![Diagnostic::new(e.code(), e.message())],
                        held => held,
                    };
                    println!("{}", diagnostics_text(&found, colored()));
                }
                Format::Json => println!(
                    "{}",
                    error_envelope(e.code(), &e.message(), &e.diagnostics())
                ),
            }
            ExitCode::from(e.exit_code())
        }
    }
}

fn lint_composition(target: Option<&str>, format: Format) -> Result<String, CliError> {
    let here = composition()?;
    let (dir, target) = match target {
        Some(target) => {
            let (dir, located) = located(&here, target)?;
            (dir, Some(located))
        }
        None => (here, None),
    };
    let report = lint(&dir, target.as_deref())?;
    let mut found: Vec<Diagnostic> = report.findings.iter().map(Finding::diagnostic).collect();
    found.extend(report.song.iter().map(sva_core::warning_diagnostic));
    if format == Format::Text {
        return Ok(diagnostics_text(&found, colored()));
    }
    Ok(success_envelope(
        &lint_data(
            &dir.display().to_string(),
            target.as_deref(),
            report.nodes,
            report.interval,
        ),
        &found,
    ))
}

fn trace_node(target: &str) -> Result<String, CliError> {
    let traced = trace(&composition()?, target)?;
    Ok(success_envelope(&trace_data(&traced), &[]))
}

fn scaffold_new(name: &str, idempotency_key: Option<&str>) -> Result<String, CliError> {
    Ok(success_envelope(
        &new_data(&scaffold(&cwd()?, name, idempotency_key)?),
        &[],
    ))
}