autors-cli 0.1.1

CANoe-inspired terminal workbench for the autors crate family
use std::path::{Path, PathBuf};

use cargo_metadata::MetadataCommand;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityGroup {
    Overview,
    Network,
    Trace,
    Calibration,
    Diagnostics,
    Simulation,
    Platform,
    Integration,
}

impl CapabilityGroup {
    pub const ALL: [Self; 8] = [
        Self::Overview,
        Self::Network,
        Self::Trace,
        Self::Calibration,
        Self::Diagnostics,
        Self::Simulation,
        Self::Platform,
        Self::Integration,
    ];

    pub fn title(self) -> &'static str {
        match self {
            Self::Overview => "Overview",
            Self::Network => "Network",
            Self::Trace => "Trace",
            Self::Calibration => "Calibration",
            Self::Diagnostics => "Diagnostics",
            Self::Simulation => "Simulation",
            Self::Platform => "Platform",
            Self::Integration => "Integration",
        }
    }

    pub fn description(self) -> &'static str {
        match self {
            Self::Overview => "Complete workspace inventory",
            Self::Network => "CAN/LIN databases, buses, frames and transport",
            Self::Trace => "CAN/LIN traces and measurement recordings",
            Self::Calibration => "ECU descriptions, values, formulas and symbols",
            Self::Diagnostics => "UDS, KWP, DoIP, ODX and flashing procedures",
            Self::Simulation => "Remaining-bus scheduling and runtime orchestration",
            Self::Platform => "Shared utilities, runtimes and native boundaries",
            Self::Integration => "C ABI and end-user workbenches",
        }
    }
}

#[derive(Debug, Clone)]
pub struct Capability {
    pub name: String,
    pub description: String,
    pub version: String,
    pub group: CapabilityGroup,
    pub features: Vec<String>,
    pub dependencies: Vec<String>,
    pub manifest_path: PathBuf,
}

impl Capability {
    pub fn detail_lines(&self) -> Vec<String> {
        let mut lines = vec![
            self.description.clone(),
            String::new(),
            format!("Version       {}", self.version),
            format!("Area          {}", self.group.title()),
            format!("Features      {}", list_or_none(&self.features)),
            format!("autors deps   {}", list_or_none(&self.dependencies)),
            String::new(),
            format!("Manifest      {}", self.manifest_path.display()),
        ];
        lines.push(String::new());
        lines.extend(
            workflows(self.name.as_str())
                .iter()
                .map(|line| (*line).to_owned()),
        );
        lines
    }
}

fn list_or_none(values: &[String]) -> String {
    if values.is_empty() {
        "none".to_owned()
    } else {
        values.join(", ")
    }
}

#[derive(Debug, Clone)]
pub struct CapabilityCatalog {
    pub workspace_root: PathBuf,
    pub capabilities: Vec<Capability>,
}

impl CapabilityCatalog {
    pub fn load(manifest_path: &Path) -> Result<Self, String> {
        let metadata = MetadataCommand::new()
            .manifest_path(manifest_path)
            .no_deps()
            .exec()
            .map_err(|error| format!("could not read workspace metadata: {error}"))?;
        let mut capabilities = metadata
            .packages
            .into_iter()
            .filter(|package| package.name.as_str().starts_with("autors-"))
            .map(|package| {
                let name = package.name.to_string();
                let mut features = package.features.keys().cloned().collect::<Vec<_>>();
                features.sort();
                let mut dependencies = package
                    .dependencies
                    .iter()
                    .map(|dependency| dependency.name.to_string())
                    .filter(|dependency| dependency.starts_with("autors-"))
                    .collect::<Vec<_>>();
                dependencies.sort();
                dependencies.dedup();
                Capability {
                    group: classify(&name),
                    name,
                    description: package
                        .description
                        .unwrap_or_else(|| "No description".to_owned()),
                    version: package.version.to_string(),
                    features,
                    dependencies,
                    manifest_path: package.manifest_path.into_std_path_buf(),
                }
            })
            .collect::<Vec<_>>();
        capabilities.sort_by(|left, right| left.name.cmp(&right.name));
        Ok(Self {
            workspace_root: metadata.workspace_root.into_std_path_buf(),
            capabilities,
        })
    }

    pub fn in_group(&self, group: CapabilityGroup, query: &str) -> Vec<usize> {
        let query = query.to_ascii_lowercase();
        self.capabilities
            .iter()
            .enumerate()
            .filter(|(_, capability)| {
                (group == CapabilityGroup::Overview || capability.group == group)
                    && (query.is_empty()
                        || capability.name.to_ascii_lowercase().contains(&query)
                        || capability.description.to_ascii_lowercase().contains(&query))
            })
            .map(|(index, _)| index)
            .collect()
    }

    pub fn group_count(&self, group: CapabilityGroup) -> usize {
        if group == CapabilityGroup::Overview {
            self.capabilities.len()
        } else {
            self.capabilities
                .iter()
                .filter(|capability| capability.group == group)
                .count()
        }
    }
}

fn classify(name: &str) -> CapabilityGroup {
    match name {
        "autors-can" | "autors-lin" | "autors-dbc" | "autors-ldf" | "autors-isotp" => {
            CapabilityGroup::Network
        }
        "autors-asc" | "autors-blf" | "autors-ltrc" | "autors-mdf" => CapabilityGroup::Trace,
        "autors-a2l" | "autors-cdf" | "autors-dcm" | "autors-datafile" | "autors-formula"
        | "autors-values" | "autors-symbols" | "autors-elf" | "autors-map" => {
            CapabilityGroup::Calibration
        }
        "autors-diag" | "autors-odx" | "autors-prm" => CapabilityGroup::Diagnostics,
        "autors-scheduler" | "autors-comm" | "autors-ccp" | "autors-xcp" => {
            CapabilityGroup::Simulation
        }
        "autors-util" | "autors-native" | "autors-runtime" => CapabilityGroup::Platform,
        "autors-ffi" | "autors-cli" => CapabilityGroup::Integration,
        _ => CapabilityGroup::Integration,
    }
}

fn workflows(name: &str) -> &'static [&'static str] {
    match name {
        "autors-a2l" => &[
            "Workflows",
            "  Parse/edit/write A2L",
            "  Browse measurements and characteristics",
        ],
        "autors-asc" => &[
            "Workflows",
            "  Read/write CANoe ASC",
            "  Inspect classic CAN and CAN FD traces",
        ],
        "autors-blf" => &[
            "Workflows",
            "  Stream compressed/uncompressed BLF",
            "  Inspect CAN, LIN and Ethernet objects",
        ],
        "autors-can" => &[
            "Workflows",
            "  Discover/configure CAN adapters",
            "  Send, receive and measure bus load",
        ],
        "autors-ccp" => &[
            "Workflows",
            "  Calibration and DAQ over CCP",
            "  ECU memory/programming operations",
        ],
        "autors-cdf" => &["Workflows", "  Read/write calibration exchange data"],
        "autors-comm" => &[
            "Workflows",
            "  Cooperative protocol polling",
            "  Shared DAQ lists and buffers",
        ],
        "autors-datafile" => &[
            "Workflows",
            "  Inspect/edit sparse images",
            "  Convert HEX, S-record, VBF, BIN, UF2 and TI-TXT",
        ],
        "autors-dbc" => &[
            "Workflows",
            "  Browse/edit CAN messages and signals",
            "  Decode raw payloads and validate databases",
        ],
        "autors-dcm" => &[
            "Workflows",
            "  Exchange DCM, PAR and MATLAB calibration sets",
        ],
        "autors-diag" => &[
            "Workflows",
            "  UDS/KWP sessions over CAN or LIN",
            "  DoIP discovery, routing and capture",
        ],
        "autors-elf" => &["Workflows", "  Inspect ELF/DWARF symbols and data types"],
        "autors-ffi" => &["Workflows", "  Consume A2L/value APIs from C and C#"],
        "autors-formula" => &["Workflows", "  Raw/physical conversions and checksums"],
        "autors-isotp" => &["Workflows", "  Segmented diagnostics over CAN and LIN"],
        "autors-ldf" => &[
            "Workflows",
            "  Browse LIN nodes, frames and schedules",
            "  Encode/decode LIN signals",
        ],
        "autors-lin" => &[
            "Workflows",
            "  Discover/configure LIN adapters",
            "  Send headers/responses and inspect traffic",
        ],
        "autors-ltrc" => &["Workflows", "  Inspect PLIN-View Pro LIN traces"],
        "autors-map" => &[
            "Workflows",
            "  Parse linker symbols and synchronize A2L addresses",
        ],
        "autors-mdf" => &["Workflows", "  Read/write MDF 3.x and 4.x measurements"],
        "autors-native" => &[
            "Workflows",
            "  Load native drivers and Seed & Key providers",
        ],
        "autors-odx" => &["Workflows", "  Resolve ODX/PDX diagnostics and flash data"],
        "autors-prm" => &["Workflows", "  Parse and execute PRM/CNF flash procedures"],
        "autors-runtime" => &[
            "Workflows",
            "  Tokio or std-backed async execution",
            "  Shared blocking bridge",
        ],
        "autors-scheduler" => &[
            "Workflows",
            "  DBC/LDF remaining-bus simulation",
            "  Runtime frame overrides and hooks",
        ],
        "autors-symbols" => &["Workflows", "  Match ELF/MAP symbols and write A2L updates"],
        "autors-util" => &[
            "Workflows",
            "  Binary, ranges, timing and logging primitives",
        ],
        "autors-values" => &[
            "Workflows",
            "  Edit scalar/curve/map calibration values",
            "  Read/write ECU memory and CDF",
        ],
        "autors-xcp" => &[
            "Workflows",
            "  Calibration, DAQ and programming over XCP",
            "  CAN, TCP, UDP and serial transports",
        ],
        "autors-cli" => &[
            "Workflows",
            "  Unified terminal capability explorer",
            "  A2L, DBC and ASC engineering workbench",
        ],
        "autors-security-flasher" => &[
            "Workflows",
            "  Discover vehicle flash configurations and plugins",
            "  Execute virtual or hardware-backed UDS programming flows",
        ],
        "autors-security-flasher-sdk" => &[
            "Workflows",
            "  Build versioned file-loader, flow and Seed & Key plugins",
            "  Cross the panic-safe C ABI boundary",
        ],
        _ => &["Workflows", "  See crate documentation for API details"],
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_known_workspace_crate_has_a_non_overview_group() {
        for name in [
            "autors-a2l",
            "autors-asc",
            "autors-blf",
            "autors-can",
            "autors-ccp",
            "autors-cdf",
            "autors-cli",
            "autors-comm",
            "autors-datafile",
            "autors-dbc",
            "autors-dcm",
            "autors-diag",
            "autors-elf",
            "autors-ffi",
            "autors-formula",
            "autors-isotp",
            "autors-ldf",
            "autors-lin",
            "autors-ltrc",
            "autors-map",
            "autors-mdf",
            "autors-native",
            "autors-odx",
            "autors-prm",
            "autors-runtime",
            "autors-scheduler",
            "autors-security-flasher",
            "autors-security-flasher-sdk",
            "autors-symbols",
            "autors-util",
            "autors-values",
            "autors-xcp",
        ] {
            assert_ne!(classify(name), CapabilityGroup::Overview, "{name}");
            assert!(workflows(name).len() > 1, "{name}");
        }
    }

    #[test]
    fn loads_the_real_workspace_catalog() {
        let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join("Cargo.toml");
        let catalog = CapabilityCatalog::load(&manifest).unwrap();
        assert!(catalog.capabilities.len() >= 30);
        assert!(catalog
            .capabilities
            .iter()
            .any(|capability| capability.name == "autors-cli"));
        assert!(catalog
            .capabilities
            .iter()
            .all(|capability| !capability.description.is_empty()));
    }
}