use std::path::PathBuf;
use clap::Args;
use serde::Serialize;
use ossctl_core::contract::{self, LoadError, Normalized};
use ossctl_core::protocol::contract::Contract;
use crate::error::CliError;
use crate::output::OutputFormat;
use crate::sys::RealFs;
#[derive(Args, Debug)]
pub struct ShowArgs {
#[arg(long, value_name = "PATH")]
pub repo_root: Option<PathBuf>,
#[arg(long)]
pub require_approved: bool,
}
#[derive(Args, Debug)]
pub struct ValidateArgs {
#[arg(long, value_name = "PATH")]
pub repo_root: Option<PathBuf>,
#[arg(long)]
pub require_approved: bool,
}
pub fn show(args: &ShowArgs, format: OutputFormat) -> Result<(), CliError> {
let repo_root = resolve_repo_root(args.repo_root.as_ref())?;
let normalized = contract::normalize(&repo_root, &RealFs).map_err(load_error_to_cli)?;
if !normalized.is_valid() {
return Err(invalid_contract_error(&normalized));
}
if args.require_approved {
require_approved(&normalized.contract)?;
}
let doc = &normalized.contract;
match format {
OutputFormat::Json => crate::output::emit_json(doc, &doc.warnings)?,
OutputFormat::Text => render_show_text(doc),
}
Ok(())
}
pub fn validate(args: &ValidateArgs, format: OutputFormat) -> Result<(), CliError> {
let repo_root = resolve_repo_root(args.repo_root.as_ref())?;
let normalized = contract::normalize(&repo_root, &RealFs).map_err(load_error_to_cli)?;
if !normalized.is_valid() {
return Err(invalid_contract_error(&normalized));
}
if args.require_approved {
require_approved(&normalized.contract)?;
}
let doc = &normalized.contract;
match format {
OutputFormat::Json => {
let body = ValidReport {
valid: true,
status: doc.status.as_str(),
maturity: doc.maturity.as_str(),
targets: doc.targets.len(),
};
crate::output::emit_json(&body, &doc.warnings)?;
}
OutputFormat::Text => {
println!(
"OK: {} normalizes cleanly (status={}, maturity={}, targets={})",
contract::CONTRACT_FILENAME,
doc.status.as_str(),
doc.maturity.as_str(),
doc.targets.len()
);
}
}
Ok(())
}
#[derive(Serialize)]
struct ValidReport {
valid: bool,
status: &'static str,
maturity: &'static str,
targets: usize,
}
fn resolve_repo_root(flag: Option<&PathBuf>) -> Result<PathBuf, CliError> {
match flag {
Some(p) => Ok(p.clone()),
None => std::env::current_dir()
.map_err(|e| CliError::system("io_error", format!("cannot resolve cwd: {e}"))),
}
}
fn load_error_to_cli(e: LoadError) -> CliError {
let code = match e {
LoadError::NotFound(_) => "contract_not_found",
LoadError::Io(..) => "io_error",
LoadError::Utf8(_) => "invalid_encoding",
};
CliError::system(code, e.to_string())
}
fn invalid_contract_error(normalized: &Normalized) -> CliError {
let problems = &normalized.problems.errors;
let message = format!(
"{} would not normalize: {} problem(s)",
contract::CONTRACT_FILENAME,
problems.len()
);
CliError::user("invalid_contract", message).with_problems(problems.clone())
}
fn require_approved(doc: &Contract) -> Result<(), CliError> {
if doc.status == ossctl_core::contract::schema::Status::Approved {
return Ok(());
}
Err(CliError::user(
"not_approved",
format!(
"contract is status '{}', not 'approved' — a mutating member refuses a draft; review \
it and set status: approved",
doc.status.as_str()
),
)
.with_invalid_value(doc.status.as_str()))
}
fn render_show_text(doc: &Contract) {
let ecos = if doc.ecosystems.is_empty() {
"[]".to_string()
} else {
doc.ecosystems
.iter()
.map(|e| e.as_str())
.collect::<Vec<_>>()
.join(", ")
};
println!("schema_version: {}", doc.schema_version);
println!("status: {}", doc.status.as_str());
println!("maturity: {}", doc.maturity.as_str());
println!("ecosystems: {ecos}");
println!("targets: {}", doc.targets.len());
println!("versioning: {}", doc.versioning.as_str());
println!("license: {}", doc.license);
for w in &doc.warnings {
println!("warning: {w}");
}
}