use crate::diagnostic::{ErrorCode, VeloqDiagnostic};
use crate::envelope::{SourceRef, TraceSpan};
use std::error::Error;
use std::path::Path;
use thiserror::Error;
pub type SourceRunError = Box<dyn Error + Send + Sync + 'static>;
pub type SourceRunResult<T> = Result<T, SourceRunError>;
#[derive(Debug, Error)]
pub enum OutputFormatError {
#[error("unknown --format `{value}` (expected: json, csv, table)")]
Unknown { value: String },
}
impl OutputFormatError {
pub fn unknown(value: &str) -> Self {
Self::Unknown {
value: value.to_string(),
}
}
}
impl VeloqDiagnostic for OutputFormatError {
fn code(&self) -> ErrorCode {
match self {
Self::Unknown { .. } => ErrorCode::new("cli.unknown-format"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OutputFormat {
Json,
Csv,
Table,
}
impl OutputFormat {
pub fn parse(s: &str) -> Result<Self, OutputFormatError> {
let normalized = s.to_ascii_lowercase();
match normalized.as_str() {
"json" => Ok(Self::Json),
"csv" => Ok(Self::Csv),
"table" | "tbl" => Ok(Self::Table),
_ => Err(OutputFormatError::unknown(s)),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Json => "json",
Self::Csv => "csv",
Self::Table => "table",
})
}
}
pub trait ProfileSource: Send + Sync {
fn kind(&self) -> &'static str;
fn version(&self) -> &'static str;
fn source_ref(&self) -> SourceRef {
SourceRef {
kind: self.kind(),
version: self.version(),
}
}
fn detect(&self, trace: &Path) -> bool;
fn compute_trace_span(&self, _trace: &Path) -> Option<TraceSpan> {
None
}
fn cli(&self) -> clap::Command;
fn run(&self, matches: &clap::ArgMatches, fmt: OutputFormat) -> SourceRunResult<i32>;
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Command;
struct FakeSource;
impl ProfileSource for FakeSource {
fn kind(&self) -> &'static str {
"fake"
}
fn version(&self) -> &'static str {
"v1"
}
fn detect(&self, p: &Path) -> bool {
p.extension().is_some_and(|e| e == "fake")
}
fn cli(&self) -> Command {
Command::new("fake").subcommand(Command::new("ping"))
}
fn run(&self, m: &clap::ArgMatches, _fmt: OutputFormat) -> SourceRunResult<i32> {
let verb = m
.subcommand_name()
.ok_or_else(|| std::io::Error::other("no subcommand"))?;
if verb != "ping" {
return Err(
std::io::Error::other(format!("unexpected subcommand `{verb}`")).into(),
);
}
Ok(0)
}
}
#[test]
fn source_ref_combines_kind_and_version() {
let s = FakeSource;
let r = s.source_ref();
assert_eq!(r.kind, "fake");
assert_eq!(r.version, "v1");
}
#[test]
fn detect_matches_by_extension() {
let s = FakeSource;
assert!(s.detect(Path::new("/tmp/t.fake")));
assert!(!s.detect(Path::new("/tmp/t.nsys-rep")));
assert!(!s.detect(Path::new("/tmp/t")));
}
#[test]
fn run_dispatches_subcommand() -> SourceRunResult<()> {
let s = FakeSource;
let m = s.cli().try_get_matches_from(["fake", "ping"])?;
assert_eq!(s.run(&m, OutputFormat::Json)?, 0);
Ok(())
}
#[test]
fn output_format_parse_round_trip() {
for s in ["json", "JSON", "csv", "Csv", "table", "tbl"] {
assert!(OutputFormat::parse(s).is_ok());
}
assert!(OutputFormat::parse("bogus").is_err());
}
}