use crate::exit_codes::ExitCode;
use copybook_governance as governance;
use governance::FeatureFlags;
#[derive(clap::Args)]
pub struct SupportArgs {
#[arg(long, value_enum, default_value = "table")]
pub format: OutputFormat,
#[arg(long)]
pub check: Option<String>,
#[arg(long, value_enum)]
pub status: Option<StatusFilter>,
#[arg(long)]
pub with_governance: bool,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
pub enum OutputFormat {
Table,
Json,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq)]
pub enum StatusFilter {
Supported,
Partial,
Planned,
NotPlanned,
}
pub fn run(args: &SupportArgs) -> anyhow::Result<ExitCode> {
let feature_flags = FeatureFlags::global();
let support_features = if args.with_governance {
governance::governance_states(feature_flags)
} else {
governance::support_states()
};
if let Some(feature_id) = &args.check {
return Ok(run_check(
feature_id,
args.with_governance,
&support_features,
feature_flags,
));
}
run_matrix_view(
args.format,
args.status,
args.with_governance,
&support_features,
)
}
fn run_check(
feature_id: &str,
with_governance: bool,
support_features: &[governance::FeatureGovernanceState],
feature_flags: &FeatureFlags,
) -> ExitCode {
let Some(support) = governance::support_matrix::find_feature(feature_id) else {
eprintln!("Error: Unknown feature ID: {feature_id}");
return ExitCode::Unknown;
};
let Some(state) = support_features
.iter()
.find(|state| state.support_id == support.id)
else {
eprintln!("Error: Governance state not found for feature: {feature_id}");
return ExitCode::Unknown;
};
match state.support_status {
governance::SupportStatus::Supported => {
println!("Feature: {}", state.support_name);
println!("Status: {:?}", state.support_status);
println!("Description: {}", state.support_description);
if let Some(doc_ref) = state.doc_ref {
println!("Documentation: {doc_ref}");
}
if with_governance {
println!("Runtime-Available: {}", state.runtime_enabled);
println!(
"Required Feature Flags: {}",
format_flags(state.required_feature_flags)
);
println!(
"Missing Feature Flags: {}",
format_flags(&state.missing_feature_flags)
);
println!("Rationale: {}", state.rationale);
if let Some(state) =
governance::governance_state_for_support_id(state.support_id, feature_flags)
{
if state.missing_feature_flags.is_empty() {
println!("Runtime gating status: enabled by feature flags");
} else {
println!("Runtime gating status: disabled by feature flags");
}
}
}
ExitCode::Ok
}
_status => {
eprintln!(
"Feature '{}' not fully supported (status: {:?}). See {}",
feature_id,
state.support_status,
state.doc_ref.unwrap_or("project documentation"),
);
if with_governance {
println!("Runtime-Available: {}", state.runtime_enabled);
println!(
"Missing Feature Flags: {}",
format_flags(&state.missing_feature_flags)
);
}
ExitCode::Encode }
}
}
fn run_matrix_view(
format: OutputFormat,
status_filter: Option<StatusFilter>,
with_governance: bool,
features: &[governance::FeatureGovernanceState],
) -> anyhow::Result<ExitCode> {
let filtered: Vec<_> = match status_filter {
Some(status_filter) => features
.iter()
.filter(|f| matches_status_filter(f.support_status, status_filter))
.cloned()
.collect(),
None => features.to_vec(),
};
match format {
OutputFormat::Table => {
if with_governance {
println!("COBOL Feature Support + Governance");
println!();
println!(
"{:<25} {:<15} {:<20} {:<16} Description",
"Feature", "Status", "Feature Flags", "Runtime",
);
println!("{}", "-".repeat(100));
for feature in &filtered {
let status_str = format!("{:?}", feature.support_status);
println!(
"{:<25} {:<15} {:<20} {:<16} {}",
feature.support_name,
status_str,
format_flags(feature.required_feature_flags),
if feature.runtime_enabled {
"enabled"
} else {
"disabled-by-flags"
},
feature.support_description,
);
}
} else {
println!("COBOL Feature Support Matrix");
println!();
println!("{:<25} {:<15} Description", "Feature", "Status");
println!("{}", "-".repeat(80));
for feature in &filtered {
println!(
"{:<25} {:<15} {}",
feature.support_name,
format!("{:?}", feature.support_status),
feature.support_description,
);
}
}
println!();
println!("Use 'copybook support --check <feature-id>' to check a specific feature.");
println!("Use 'copybook support --format json' for machine-readable output.");
if with_governance {
println!(
"Use 'copybook support --with-governance' to include runtime flag linkage."
);
}
}
OutputFormat::Json => {
let json = if with_governance {
serde_json::to_string_pretty(&filtered)?
} else {
let basic: Vec<_> = filtered
.iter()
.filter_map(|feature| {
governance::support_matrix::find_feature_by_id(feature.support_id)
})
.collect();
serde_json::to_string_pretty(&basic)?
};
println!("{json}");
}
}
Ok(ExitCode::Ok)
}
fn format_flags(flags: &[governance::Feature]) -> String {
if flags.is_empty() {
"none".to_string()
} else {
flags
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
}
}
fn matches_status_filter(status: governance::SupportStatus, filter: StatusFilter) -> bool {
use governance::SupportStatus;
matches!(
(status, filter),
(SupportStatus::Supported, StatusFilter::Supported)
| (SupportStatus::Partial, StatusFilter::Partial)
| (SupportStatus::Planned, StatusFilter::Planned)
| (SupportStatus::NotPlanned, StatusFilter::NotPlanned)
)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_find_feature_level88() {
let feature = governance::support_matrix::find_feature("level-88");
assert!(feature.is_some());
let f = feature.expect("Feature should exist");
assert_eq!(f.name, "LEVEL 88 condition names");
}
#[test]
fn test_find_feature_unknown() {
let feature = governance::support_matrix::find_feature("no-such-feature");
assert!(feature.is_none());
}
#[test]
fn test_all_features_nonempty() {
let features = governance::support_matrix::all_features();
assert!(!features.is_empty());
}
#[test]
fn test_json_feature_set_equality() {
use std::collections::HashSet;
let features = governance::support_matrix::all_features();
let json = serde_json::to_string(&features).expect("Failed to serialize");
let parsed: Vec<serde_json::Value> =
serde_json::from_str(&json).expect("Failed to parse JSON");
let json_ids: HashSet<String> = parsed
.iter()
.filter_map(|v| v.get("id").and_then(|id| id.as_str()).map(String::from))
.collect();
let registry_ids: HashSet<String> = features
.iter()
.filter_map(|f| serde_plain::to_string(&f.id).ok())
.collect();
assert_eq!(
json_ids, registry_ids,
"JSON feature IDs must match registry exactly"
);
assert_eq!(
json_ids.len(),
features.len(),
"All features must be represented"
);
}
#[test]
fn test_format_flags_none() {
assert_eq!(format_flags(&[]), "none");
}
#[test]
fn test_format_flags_values() {
let flags = vec![governance::Feature::SignSeparate];
assert_eq!(format_flags(&flags), "sign_separate");
}
}