use std::{collections::HashMap, path::PathBuf};
use async_trait::async_trait;
use axum::body::Body;
use axum::http::Method;
use serde_json::{json, Value};
use tracing::debug;
use crate::cli::commands::{QddCommands, ScaffoldCommands};
use crate::cli::{
AnalyzeCommands, Commands, ComplexityOutputFormat, ContextFormat, DagType, OutputFormat,
};
use crate::models::churn::ChurnOutputFormat;
use crate::unified_protocol::{
CliContext, Protocol, ProtocolAdapter, ProtocolError, UnifiedRequest, UnifiedResponse,
};
pub struct CliAdapter;
impl CliAdapter {
#[must_use]
pub fn new() -> Self {
Self
}
fn decode_command(
&self,
command: &Commands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
match command {
Commands::Generate {
category,
template,
params,
output,
create_dirs,
} => Self::decode_generate(category, template, params, output, create_dirs),
Commands::Scaffold { command } => match command {
ScaffoldCommands::Project {
toolchain,
templates,
params,
parallel,
} => Self::decode_scaffold(toolchain, templates, params, *parallel),
_ => Err(ProtocolError::UnsupportedProtocol(
"Agent scaffolding not supported via unified protocol".to_string(),
)),
},
Commands::List {
toolchain,
category,
format,
} => Self::decode_list(toolchain, category, format),
Commands::Search {
query,
toolchain,
limit,
} => Self::decode_search(query, toolchain, *limit),
Commands::Validate { uri, params } => Self::decode_validate(uri, params),
Commands::Context {
toolchain,
project_path,
output,
format,
include_large_files: _,
skip_expensive_metrics: _,
} => Self::decode_context(toolchain.as_deref(), project_path, output, format),
Commands::Analyze(analyze_cmd) => Self::decode_analyze_command(analyze_cmd),
Commands::Qdd(_) => Self::cli_only_command_error(),
Commands::Demo {
path,
url,
format,
no_browser,
port,
cli,
target_nodes,
centrality_threshold,
merge_threshold,
..
} => Self::decode_demo(
path,
url,
format,
*no_browser,
port,
*cli,
*target_nodes,
*centrality_threshold,
*merge_threshold,
),
Commands::Serve {
host,
port,
cors,
transport: _,
} => Self::decode_serve(host, *port, *cors),
Commands::Diagnose(_)
| Commands::QualityGate { .. }
| Commands::Report { .. }
| Commands::Enforce(_)
| Commands::Refactor(_)
| Commands::Roadmap(_)
| Commands::Test { .. }
| Commands::Memory { .. }
| Commands::Cache { .. }
| Commands::Telemetry { .. }
| Commands::Config { .. }
| Commands::Agent { .. }
| Commands::Tdg { .. } => Self::cli_only_command_error(),
}
}
fn decode_generate(
category: &str,
template: &str,
params: &[(String, Value)],
output: &Option<std::path::PathBuf>,
create_dirs: &bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params_map: HashMap<String, Value> = params.iter().cloned().collect();
let body = json!({
"template_uri": format!("template://{}/{}", category, template),
"parameters": params_map,
"output_path": output,
"create_dirs": create_dirs
});
Ok((Method::POST, "/api/v1/generate".to_string(), body, None))
}
fn decode_scaffold(
toolchain: &str,
templates: &[String],
params: &[(String, Value)],
parallel: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params_map: HashMap<String, Value> = params.iter().cloned().collect();
let body = json!({
"toolchain": toolchain,
"templates": templates,
"parameters": params_map,
"parallel": ¶llel
});
Ok((Method::POST, "/api/v1/scaffold".to_string(), body, None))
}
fn decode_list(
toolchain: &Option<String>,
category: &Option<String>,
format: &OutputFormat,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let mut query_params = Vec::new();
if let Some(tc) = toolchain {
query_params.push(format!("toolchain={tc}"));
}
if let Some(cat) = category {
query_params.push(format!("category={cat}"));
}
if !query_params.is_empty() {
query_params.push(format!("format={format:?}").to_lowercase());
}
let query_string = if query_params.is_empty() {
String::new()
} else {
format!("?{}", query_params.join("&"))
};
Ok((
Method::GET,
format!("/api/v1/templates{query_string}"),
json!({}),
Some(format.clone()),
))
}
fn decode_search(
query: &str,
toolchain: &Option<String>,
limit: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"query": query,
"toolchain": toolchain,
"limit": &limit
});
Ok((Method::POST, "/api/v1/search".to_string(), body, None))
}
fn decode_validate(
uri: &str,
params: &[(String, Value)],
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params_map: HashMap<String, Value> = params.iter().cloned().collect();
let body = json!({
"template_uri": uri,
"parameters": params_map
});
Ok((Method::POST, "/api/v1/validate".to_string(), body, None))
}
fn decode_context(
toolchain: Option<&str>,
project_path: &std::path::Path,
output: &Option<std::path::PathBuf>,
format: &ContextFormat,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"toolchain": toolchain,
"project_path": project_path.to_string_lossy(),
"output_path": output,
"format": format_to_string(format)
});
Ok((
Method::POST,
"/api/v1/analyze/context".to_string(),
body,
Some(OutputFormat::Json),
))
}
fn decode_analyze_command(
analyze_cmd: &AnalyzeCommands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let command_category = Self::get_analyze_command_category(analyze_cmd);
match command_category {
AnalyzeCommandCategory::Basic => Self::dispatch_basic_analysis(analyze_cmd),
AnalyzeCommandCategory::Advanced => Self::dispatch_advanced_analysis(analyze_cmd),
AnalyzeCommandCategory::Structural => Self::dispatch_structural_analysis(analyze_cmd),
AnalyzeCommandCategory::Specialized => Self::dispatch_specialized_analysis(analyze_cmd),
}
}
fn dispatch_basic_analysis(
analyze_cmd: &AnalyzeCommands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
match analyze_cmd {
AnalyzeCommands::Churn {
project_path,
days,
format,
output,
top_files,
include: _,
exclude: _,
} => Self::decode_analyze_churn(project_path, *days, format, output, *top_files),
AnalyzeCommands::Complexity {
path,
project_path,
file,
files,
toolchain,
format,
output,
max_cyclomatic,
max_cognitive,
include,
watch,
top_files,
fail_on_violation: _,
timeout: _,
} => Self::decode_analyze_complexity_with_migration(
path,
project_path,
file,
files,
toolchain,
format,
output,
max_cyclomatic,
max_cognitive,
include,
*watch,
*top_files,
),
AnalyzeCommands::DeadCode {
path,
format,
top_files,
include_unreachable,
min_dead_lines,
include_tests,
output,
fail_on_violation: _,
max_percentage: _,
timeout: _,
include: _,
exclude: _,
} => Self::decode_analyze_dead_code(
path,
format,
top_files,
*include_unreachable,
*min_dead_lines,
*include_tests,
output,
),
AnalyzeCommands::Satd {
path,
format,
severity,
critical_only,
include_tests,
strict,
evolution,
days,
metrics,
output,
top_files,
fail_on_violation: _,
timeout: _,
include: _,
exclude: _,
} => Self::decode_analyze_satd(
path,
format,
severity,
*critical_only,
*include_tests,
*strict,
*evolution,
*days,
*metrics,
output,
*top_files,
),
AnalyzeCommands::Tdg {
path,
threshold,
top_files,
format,
include_components,
output,
critical_only,
verbose,
} => Self::decode_analyze_tdg(
path,
output,
format,
*threshold,
*critical_only,
*top_files,
*include_components,
*verbose,
),
AnalyzeCommands::LintHotspot {
project_path,
file,
format,
max_density,
min_confidence,
enforce,
dry_run,
enforcement_metadata,
output,
perf,
clippy_flags,
top_files,
include: _,
exclude: _,
} => Self::decode_analyze_lint_hotspot(
project_path,
file,
format,
max_density,
min_confidence,
enforce,
dry_run,
enforcement_metadata,
output,
perf,
clippy_flags,
top_files,
),
_ => Err(ProtocolError::UnsupportedProtocol(
"Command not supported in basic analysis dispatch".to_string(),
)),
}
}
fn dispatch_advanced_analysis(
analyze_cmd: &AnalyzeCommands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
match analyze_cmd {
AnalyzeCommands::DeepContext {
project_path,
output,
format,
full,
include,
exclude,
period_days,
dag_type,
max_depth,
include_patterns,
exclude_patterns,
cache_strategy,
parallel,
verbose,
top_files: _,
} => Self::decode_analyze_deep_context(
project_path,
output,
format,
*full,
include,
exclude,
*period_days,
dag_type,
max_depth,
include_patterns,
exclude_patterns,
cache_strategy,
parallel,
*verbose,
),
AnalyzeCommands::Comprehensive {
project_path,
file,
files,
format,
include_duplicates,
include_dead_code,
include_defects,
include_complexity,
include_tdg,
confidence_threshold,
min_lines,
include,
exclude,
output,
perf,
executive_summary,
top_files,
} => Self::decode_analyze_comprehensive(
project_path,
file,
files,
format,
include_duplicates,
include_dead_code,
include_defects,
include_complexity,
include_tdg,
confidence_threshold,
min_lines,
include,
exclude,
output,
perf,
executive_summary,
top_files,
),
AnalyzeCommands::DefectPrediction {
project_path,
confidence_threshold,
min_lines,
include_low_confidence,
format,
high_risk_only,
include_recommendations,
include,
exclude,
output,
perf,
top_files,
} => Self::decode_analyze_defect_prediction(
project_path,
confidence_threshold,
min_lines,
include_low_confidence,
format,
high_risk_only,
include_recommendations,
include,
exclude,
output,
perf,
top_files,
),
AnalyzeCommands::Duplicates {
project_path,
detection_type,
threshold,
min_lines,
max_tokens,
format,
perf,
include,
exclude,
output,
top_files,
} => Self::decode_analyze_duplicates(
project_path,
detection_type,
threshold,
min_lines,
max_tokens,
format,
perf,
include,
exclude,
output,
top_files,
),
AnalyzeCommands::BigO {
project_path,
format,
confidence_threshold,
analyze_space,
include,
exclude,
output,
perf,
high_complexity_only,
top_files,
} => Self::decode_analyze_big_o(
project_path,
format,
confidence_threshold,
analyze_space,
include,
exclude,
output,
perf,
high_complexity_only,
top_files,
),
_ => Err(ProtocolError::UnsupportedProtocol(
"Command not supported in advanced analysis dispatch".to_string(),
)),
}
}
fn dispatch_structural_analysis(
analyze_cmd: &AnalyzeCommands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
match analyze_cmd {
AnalyzeCommands::Dag {
dag_type,
project_path,
output,
max_depth,
target_nodes,
filter_external,
show_complexity,
include_duplicates,
include_dead_code,
enhanced,
} => Self::decode_analyze_dag(
dag_type,
project_path,
output,
max_depth,
target_nodes,
*filter_external,
*show_complexity,
*include_duplicates,
*include_dead_code,
*enhanced,
),
AnalyzeCommands::GraphMetrics {
project_path,
metrics,
pagerank_seeds,
damping_factor,
max_iterations,
convergence_threshold,
format,
include,
exclude,
output,
export_graphml,
perf,
top_k,
min_centrality,
} => Self::decode_analyze_graph_metrics(
project_path,
metrics,
pagerank_seeds,
damping_factor,
max_iterations,
convergence_threshold,
format,
include,
exclude,
output,
export_graphml,
perf,
top_k,
min_centrality,
),
AnalyzeCommands::SymbolTable {
project_path,
format,
query,
filter,
include,
exclude,
show_unreferenced,
show_references,
output,
perf,
top_files,
} => Self::decode_analyze_symbol_table(
project_path,
format,
query,
filter,
include,
exclude,
show_unreferenced,
show_references,
output,
perf,
top_files,
),
AnalyzeCommands::NameSimilarity {
project_path,
query,
top_k,
phonetic,
scope,
threshold,
format,
include,
exclude,
output,
perf,
fuzzy,
case_sensitive,
} => Self::decode_analyze_name_similarity(
project_path,
query,
top_k,
phonetic,
scope,
threshold,
format,
include,
exclude,
output,
perf,
fuzzy,
case_sensitive,
),
_ => Err(ProtocolError::UnsupportedProtocol(
"Command not supported in structural analysis dispatch".to_string(),
)),
}
}
fn dispatch_specialized_analysis(
analyze_cmd: &AnalyzeCommands,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
match analyze_cmd {
AnalyzeCommands::Makefile {
path,
rules,
format,
fix,
gnu_version,
top_files,
} => Self::decode_analyze_makefile(path, rules, format, fix, gnu_version, top_files),
AnalyzeCommands::Provability {
project_path,
functions,
analysis_depth,
format,
high_confidence_only,
include_evidence,
output,
top_files,
} => Self::decode_analyze_provability(
project_path,
functions,
*analysis_depth,
format,
*high_confidence_only,
*include_evidence,
output,
*top_files,
),
AnalyzeCommands::ProofAnnotations {
project_path,
format,
high_confidence_only,
include_evidence,
property_type,
verification_method,
output,
perf,
clear_cache,
top_files,
} => Self::decode_analyze_proof_annotations(
project_path,
format,
high_confidence_only,
include_evidence,
property_type,
verification_method,
output,
perf,
clear_cache,
top_files,
),
AnalyzeCommands::IncrementalCoverage {
project_path,
base_branch,
target_branch,
format,
coverage_threshold,
changed_files_only,
detailed,
output,
perf,
cache_dir,
force_refresh,
top_files,
} => Self::decode_analyze_incremental_coverage(
project_path,
base_branch,
target_branch,
format,
coverage_threshold,
changed_files_only,
detailed,
output,
perf,
cache_dir,
force_refresh,
top_files,
),
AnalyzeCommands::AssemblyScript { top_files: _, .. } => {
Self::decode_analyze_assemblyscript()
}
AnalyzeCommands::WebAssembly { top_files: _, .. } => Self::decode_analyze_webassembly(),
_ => Err(ProtocolError::UnsupportedProtocol(
"Command not supported in specialized analysis dispatch".to_string(),
)),
}
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_complexity_with_migration(
path: &std::path::Path,
project_path: &Option<std::path::PathBuf>,
file: &Option<std::path::PathBuf>,
files: &[std::path::PathBuf],
toolchain: &Option<String>,
format: &ComplexityOutputFormat,
output: &Option<std::path::PathBuf>,
max_cyclomatic: &Option<u16>,
max_cognitive: &Option<u16>,
include: &[String],
watch: bool,
top_files: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let analysis_path = if let Some(deprecated_path) = project_path {
deprecated_path.as_ref()
} else {
path
};
Self::decode_analyze_complexity(
analysis_path,
file,
files,
toolchain,
format,
output,
max_cyclomatic,
max_cognitive,
include,
watch,
top_files,
)
}
fn decode_analyze_churn(
project_path: &std::path::Path,
days: u32,
format: &ChurnOutputFormat,
output: &Option<std::path::PathBuf>,
top_files: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": project_path.to_string_lossy(),
"period_days": &days,
"format": churn_format_to_string(format),
"output_path": output,
"top_files": top_files
});
Ok((
Method::POST,
"/api/v1/analyze/churn".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_complexity(
project_path: &std::path::Path,
file: &Option<std::path::PathBuf>,
files: &[std::path::PathBuf],
toolchain: &Option<String>,
format: &ComplexityOutputFormat,
output: &Option<std::path::PathBuf>,
max_cyclomatic: &Option<u16>,
max_cognitive: &Option<u16>,
include: &[String],
watch: bool,
top_files: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": project_path.to_string_lossy(),
"file": file.as_ref().map(|f| f.to_string_lossy()),
"files": files.iter().map(|f| f.to_string_lossy()).collect::<Vec<_>>(),
"toolchain": toolchain,
"format": complexity_format_to_string(format),
"output_path": output,
"max_cyclomatic": max_cyclomatic,
"max_cognitive": max_cognitive,
"include_patterns": include,
"watch": &watch,
"top_files": &top_files
});
Ok((
Method::POST,
"/api/v1/analyze/complexity".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_dag(
dag_type: &DagType,
project_path: &std::path::Path,
output: &Option<std::path::PathBuf>,
max_depth: &Option<usize>,
target_nodes: &Option<usize>,
filter_external: bool,
show_complexity: bool,
include_duplicates: bool,
include_dead_code: bool,
enhanced: bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": project_path.to_string_lossy(),
"dag_type": dag_type_to_string(dag_type),
"output_path": output,
"max_depth": max_depth,
"target_nodes": target_nodes,
"filter_external": &filter_external,
"show_complexity": &show_complexity,
"include_duplicates": &include_duplicates,
"include_dead_code": &include_dead_code,
"enhanced": &enhanced
});
Ok((
Method::POST,
"/api/v1/analyze/dag".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_dead_code(
path: &std::path::Path,
format: &crate::cli::DeadCodeOutputFormat,
top_files: &Option<usize>,
include_unreachable: bool,
min_dead_lines: usize,
include_tests: bool,
output: &Option<std::path::PathBuf>,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": path.to_string_lossy(),
"format": dead_code_format_to_string(format),
"top_files": top_files,
"include_unreachable": &include_unreachable,
"min_dead_lines": &min_dead_lines,
"include_tests": &include_tests,
"output_path": output
});
Ok((
Method::POST,
"/api/v1/analyze/dead-code".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_satd(
path: &std::path::Path,
format: &crate::cli::SatdOutputFormat,
severity: &Option<crate::cli::SatdSeverity>,
critical_only: bool,
include_tests: bool,
strict: bool,
evolution: bool,
days: u32,
metrics: bool,
output: &Option<std::path::PathBuf>,
top_files: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": path.to_string_lossy(),
"format": satd_format_to_string(format),
"severity": severity.as_ref().map(satd_severity_to_string),
"critical_only": &critical_only,
"include_tests": &include_tests,
"strict": &strict,
"evolution": &evolution,
"days": &days,
"metrics": &metrics,
"output_path": output,
"top_files": &top_files
});
Ok((
Method::POST,
"/api/v1/analyze/satd".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_deep_context(
project_path: &std::path::Path,
output: &Option<std::path::PathBuf>,
format: &crate::cli::DeepContextOutputFormat,
full: bool,
include: &[String],
exclude: &[String],
period_days: u32,
dag_type: &crate::cli::DeepContextDagType,
max_depth: &Option<usize>,
include_patterns: &[String],
exclude_patterns: &[String],
cache_strategy: &crate::cli::DeepContextCacheStrategy,
parallel: &Option<usize>,
verbose: bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": project_path.to_string_lossy(),
"output_path": output,
"format": deep_context_format_to_string(format),
"full": &full,
"include": include,
"exclude": exclude,
"period_days": &period_days,
"dag_type": deep_context_dag_type_to_string(dag_type),
"max_depth": max_depth,
"include_patterns": include_patterns,
"exclude_patterns": exclude_patterns,
"cache_strategy": deep_context_cache_strategy_to_string(cache_strategy),
"parallel": parallel,
"verbose": &verbose
});
Ok((
Method::POST,
"/api/v1/analyze/deep-context".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_tdg(
path: &std::path::Path,
output: &Option<std::path::PathBuf>,
format: &crate::cli::TdgOutputFormat,
threshold: f64,
critical_only: bool,
top: usize,
include_components: bool,
verbose: bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": path.to_string_lossy(),
"output_path": output,
"format": tdg_format_to_string(format),
"threshold": &threshold,
"critical_only": &critical_only,
"top": &top,
"include_components": &include_components,
"verbose": &verbose
});
Ok((
Method::POST,
"/api/v1/analyze/tdg".to_string(),
body,
Some(OutputFormat::Json),
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_provability(
project_path: &std::path::Path,
functions: &[String],
analysis_depth: usize,
format: &crate::cli::ProvabilityOutputFormat,
high_confidence_only: bool,
include_evidence: bool,
output: &Option<std::path::PathBuf>,
top_files: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"project_path": project_path.to_string_lossy(),
"functions": if functions.is_empty() { None } else { Some(functions) },
"analysis_depth": &analysis_depth,
"format": provability_format_to_string(format),
"high_confidence_only": &high_confidence_only,
"include_evidence": &include_evidence,
"output_path": output,
"top_files": &top_files
});
Ok((
Method::POST,
"/api/v1/analyze/provability".to_string(),
body,
Some(OutputFormat::Json),
))
}
fn decode_serve(
host: &str,
port: u16,
cors: bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"host": host,
"port": port,
"cors": cors
});
Ok((Method::POST, "/api/v1/serve".to_string(), body, None))
}
#[allow(clippy::too_many_arguments)]
fn decode_demo(
path: &Option<PathBuf>,
url: &Option<String>,
format: &OutputFormat,
no_browser: bool,
port: &Option<u16>,
cli: bool,
target_nodes: usize,
centrality_threshold: f64,
merge_threshold: usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let body = json!({
"path": path.as_ref().map(|p| p.to_string_lossy().to_string()),
"url": url,
"format": format!("{format:?}").to_lowercase(),
"no_browser": &no_browser,
"port": port,
"cli_mode": &cli,
"target_nodes": &target_nodes,
"centrality_threshold": ¢rality_threshold,
"merge_threshold": &merge_threshold
});
Ok((Method::POST, "/api/v1/demo".to_string(), body, None))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_lint_hotspot(
project_path: &std::path::Path,
file: &Option<PathBuf>,
format: &crate::cli::LintHotspotOutputFormat,
max_density: &f64,
min_confidence: &f64,
enforce: &bool,
dry_run: &bool,
enforcement_metadata: &bool,
output: &Option<PathBuf>,
perf: &bool,
clippy_flags: &String,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"file": file,
"format": format,
"max_density": max_density,
"min_confidence": min_confidence,
"enforce": enforce,
"dry_run": dry_run,
"enforcement_metadata": enforcement_metadata,
"output": output,
"perf": perf,
"clippy_flags": clippy_flags,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/lint-hotspot".to_string(),
params,
None,
))
}
fn decode_analyze_makefile(
path: &std::path::Path,
rules: &Vec<String>,
format: &crate::cli::MakefileOutputFormat,
fix: &bool,
gnu_version: &String,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"path": path,
"rules": rules,
"fix": fix,
"gnu_version": gnu_version,
"format": format,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/makefile".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_duplicates(
project_path: &std::path::Path,
detection_type: &crate::cli::DuplicateType,
threshold: &f32,
min_lines: &usize,
max_tokens: &usize,
format: &crate::cli::DuplicateOutputFormat,
perf: &bool,
include: &Option<String>,
exclude: &Option<String>,
output: &Option<PathBuf>,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"detection_type": detection_type,
"threshold": threshold,
"min_lines": min_lines,
"max_tokens": max_tokens,
"format": format,
"perf": perf,
"include": include,
"exclude": exclude,
"output": output,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/duplicates".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_defect_prediction(
project_path: &std::path::Path,
confidence_threshold: &f32,
min_lines: &usize,
include_low_confidence: &bool,
format: &crate::cli::DefectPredictionOutputFormat,
high_risk_only: &bool,
include_recommendations: &bool,
include: &Option<String>,
exclude: &Option<String>,
output: &Option<PathBuf>,
perf: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"confidence_threshold": confidence_threshold,
"min_lines": min_lines,
"include_low_confidence": include_low_confidence,
"format": format,
"high_risk_only": high_risk_only,
"include_recommendations": include_recommendations,
"include": include,
"exclude": exclude,
"output": output,
"perf": perf,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/defect-prediction".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_comprehensive(
project_path: &std::path::Path,
file: &Option<PathBuf>,
files: &[PathBuf],
format: &crate::cli::ComprehensiveOutputFormat,
include_duplicates: &bool,
include_dead_code: &bool,
include_defects: &bool,
include_complexity: &bool,
include_tdg: &bool,
confidence_threshold: &f32,
min_lines: &usize,
include: &Option<String>,
exclude: &Option<String>,
output: &Option<PathBuf>,
perf: &bool,
executive_summary: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"file": file,
"files": files.iter().map(|f| f.to_string_lossy()).collect::<Vec<_>>(),
"format": format,
"include_duplicates": include_duplicates,
"include_dead_code": include_dead_code,
"include_defects": include_defects,
"include_complexity": include_complexity,
"include_tdg": include_tdg,
"confidence_threshold": confidence_threshold,
"min_lines": min_lines,
"include": include,
"exclude": exclude,
"output": output,
"perf": perf,
"executive_summary": executive_summary,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/comprehensive".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_graph_metrics(
project_path: &std::path::Path,
metrics: &[crate::cli::GraphMetricType],
pagerank_seeds: &[String],
damping_factor: &f32,
max_iterations: &usize,
convergence_threshold: &f64,
format: &crate::cli::GraphMetricsOutputFormat,
include: &Option<String>,
exclude: &Option<String>,
output: &Option<PathBuf>,
export_graphml: &bool,
perf: &bool,
top_k: &usize,
min_centrality: &f64,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"metrics": metrics.iter().map(graph_metric_type_to_string).collect::<Vec<_>>(),
"pagerank_seeds": pagerank_seeds,
"damping_factor": damping_factor,
"max_iterations": max_iterations,
"convergence_threshold": convergence_threshold,
"format": graph_metrics_format_to_string(format),
"include": include,
"exclude": exclude,
"output": output,
"export_graphml": export_graphml,
"perf": perf,
"top_k": top_k,
"min_centrality": min_centrality,
});
Ok((
Method::POST,
"/api/v1/analyze/graph-metrics".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_name_similarity(
project_path: &std::path::Path,
query: &str,
top_k: &usize,
phonetic: &bool,
scope: &crate::cli::SearchScope,
threshold: &f32,
format: &crate::cli::NameSimilarityOutputFormat,
include: &Option<String>,
exclude: &Option<String>,
output: &Option<PathBuf>,
perf: &bool,
fuzzy: &bool,
case_sensitive: &bool,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"query": query,
"top_k": top_k,
"phonetic": phonetic,
"scope": match scope {
crate::cli::SearchScope::Functions => "functions",
crate::cli::SearchScope::Types => "types",
crate::cli::SearchScope::Variables => "variables",
crate::cli::SearchScope::All => "all",
},
"threshold": threshold,
"format": name_similarity_format_to_string(format),
"include": include,
"exclude": exclude,
"output": output,
"perf": perf,
"fuzzy": fuzzy,
"case_sensitive": case_sensitive,
});
Ok((
Method::POST,
"/api/v1/analyze/name-similarity".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_proof_annotations(
project_path: &std::path::Path,
format: &crate::cli::ProofAnnotationOutputFormat,
high_confidence_only: &bool,
include_evidence: &bool,
property_type: &Option<crate::cli::PropertyTypeFilter>,
verification_method: &Option<crate::cli::VerificationMethodFilter>,
output: &Option<PathBuf>,
perf: &bool,
clear_cache: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"format": proof_annotation_format_to_string(format),
"high_confidence_only": high_confidence_only,
"include_evidence": include_evidence,
"property_type": property_type.as_ref().map(property_type_filter_to_string),
"verification_method": verification_method.as_ref().map(verification_method_filter_to_string),
"output": output,
"perf": perf,
"clear_cache": clear_cache,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/proof-annotations".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_incremental_coverage(
project_path: &std::path::Path,
base_branch: &String,
target_branch: &Option<String>,
format: &crate::cli::IncrementalCoverageOutputFormat,
coverage_threshold: &f64,
changed_files_only: &bool,
detailed: &bool,
output: &Option<PathBuf>,
perf: &bool,
cache_dir: &Option<PathBuf>,
force_refresh: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"base_branch": base_branch,
"target_branch": target_branch,
"format": incremental_coverage_format_to_string(format),
"coverage_threshold": coverage_threshold,
"changed_files_only": changed_files_only,
"detailed": detailed,
"output": output,
"perf": perf,
"cache_dir": cache_dir,
"force_refresh": force_refresh,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/incremental-coverage".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_symbol_table(
project_path: &std::path::Path,
format: &crate::cli::SymbolTableOutputFormat,
query: &Option<String>,
filter: &Option<crate::cli::SymbolTypeFilter>,
include: &Vec<String>,
exclude: &Vec<String>,
show_unreferenced: &bool,
show_references: &bool,
output: &Option<PathBuf>,
perf: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"format": symbol_table_format_to_string(format),
"query": query,
"filter": filter.as_ref().map(symbol_type_filter_to_string),
"include": include,
"exclude": exclude,
"show_unreferenced": show_unreferenced,
"show_references": show_references,
"output": output,
"perf": perf,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/symbol-table".to_string(),
params,
None,
))
}
#[allow(clippy::too_many_arguments)]
fn decode_analyze_big_o(
project_path: &std::path::Path,
format: &crate::cli::BigOOutputFormat,
confidence_threshold: &u8,
analyze_space: &bool,
include: &Vec<String>,
exclude: &Vec<String>,
output: &Option<PathBuf>,
perf: &bool,
high_complexity_only: &bool,
top_files: &usize,
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
let params = json!({
"project_path": project_path,
"format": big_o_format_to_string(format),
"confidence_threshold": confidence_threshold,
"analyze_space": analyze_space,
"include": include,
"exclude": exclude,
"output": output,
"perf": perf,
"high_complexity_only": high_complexity_only,
"top_files": top_files,
});
Ok((
Method::POST,
"/api/v1/analyze/big-o".to_string(),
params,
None,
))
}
fn decode_analyze_assemblyscript(
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
Ok((
Method::POST,
"/api/v1/analyze/assemblyscript".to_string(),
json!({}),
None,
))
}
fn decode_analyze_webassembly(
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
Ok((
Method::POST,
"/api/v1/analyze/webassembly".to_string(),
json!({}),
None,
))
}
fn cli_only_command_error(
) -> Result<(Method, String, Value, Option<OutputFormat>), ProtocolError> {
Err(ProtocolError::InvalidFormat(
"Command should be handled directly by CLI".to_string(),
))
}
fn format_to_extension_string(format: &OutputFormat) -> &'static str {
match format {
OutputFormat::Json => "json",
OutputFormat::Table => "table",
OutputFormat::Yaml => "yaml",
}
}
}
impl Default for CliAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ProtocolAdapter for CliAdapter {
type Input = CliInput;
type Output = CliOutput;
fn protocol(&self) -> Protocol {
Protocol::Cli
}
async fn decode(&self, input: Self::Input) -> Result<UnifiedRequest, ProtocolError> {
debug!("Decoding CLI input: {:?}", input.command_name);
let (method, path, body, output_format) = self.decode_command(&input.command)?;
let cli_context = CliContext {
command: input.command_name.clone(),
args: input.raw_args.clone(),
};
let mut unified_request = UnifiedRequest::new(method, path.clone())
.with_body(Body::from(serde_json::to_vec(&body)?))
.with_header("content-type", "application/json")
.with_extension("protocol", Protocol::Cli)
.with_extension("cli_context", cli_context);
if let Some(format) = output_format {
let format_string = Self::format_to_extension_string(&format);
unified_request = unified_request.with_extension("output_format", format_string);
}
debug!(
command = %input.command_name,
path = %path,
"Decoded CLI request"
);
Ok(unified_request)
}
async fn encode(&self, response: UnifiedResponse) -> Result<Self::Output, ProtocolError> {
debug!(status = %response.status, "Encoding CLI response");
let body_bytes = axum::body::to_bytes(response.body, usize::MAX)
.await
.map_err(|e| {
ProtocolError::EncodeError(format!("Failed to read response body: {e}"))
})?;
if response.status.is_success() {
let content = String::from_utf8(body_bytes.to_vec()).map_err(|e| {
ProtocolError::EncodeError(format!("Invalid UTF-8 in response: {e}"))
})?;
Ok(CliOutput::Success {
content,
exit_code: 0,
})
} else {
let error_data: Result<Value, _> = serde_json::from_slice(&body_bytes);
let error_message = match error_data {
Ok(json) => json
.get("error")
.and_then(|e| e.as_str())
.unwrap_or("Unknown error")
.to_string(),
Err(_) => String::from_utf8_lossy(&body_bytes).to_string(),
};
let exit_code = match response.status.as_u16() {
400..=499 => 1, 500..=599 => 2, _ => 1,
};
Ok(CliOutput::Error {
message: error_message,
exit_code,
})
}
}
}
pub struct CliInput {
pub command: Commands,
pub command_name: String,
pub raw_args: Vec<String>,
}
impl CliInput {
#[must_use]
pub fn new(command: Commands, command_name: String, raw_args: Vec<String>) -> Self {
Self {
command,
command_name,
raw_args,
}
}
fn get_analyze_command_name(analyze_cmd: &AnalyzeCommands) -> &'static str {
let category = CliAdapter::get_analyze_command_category(analyze_cmd);
match category {
AnalyzeCommandCategory::Basic => Self::get_basic_analyze_command_name(analyze_cmd),
AnalyzeCommandCategory::Advanced => {
Self::get_advanced_analyze_command_name(analyze_cmd)
}
AnalyzeCommandCategory::Structural => {
Self::get_structural_analyze_command_name(analyze_cmd)
}
AnalyzeCommandCategory::Specialized => {
Self::get_specialized_analyze_command_name(analyze_cmd)
}
}
}
fn get_qdd_command_name(qdd_cmd: &QddCommands) -> &'static str {
match qdd_cmd {
QddCommands::Create { .. } => "qdd-create",
QddCommands::Refactor { .. } => "qdd-refactor",
QddCommands::Validate { .. } => "qdd-validate",
}
}
fn get_basic_analyze_command_name(analyze_cmd: &AnalyzeCommands) -> &'static str {
match analyze_cmd {
AnalyzeCommands::Churn { .. } => "analyze-churn",
AnalyzeCommands::Complexity { .. } => "analyze-complexity",
AnalyzeCommands::DeadCode { .. } => "analyze-dead-code",
AnalyzeCommands::Satd { .. } => "analyze-satd",
AnalyzeCommands::Tdg { .. } => "analyze-tdg",
AnalyzeCommands::LintHotspot { .. } => "analyze-lint-hotspot",
_ => unreachable!("Non-basic command passed to basic command name extractor"),
}
}
fn get_advanced_analyze_command_name(analyze_cmd: &AnalyzeCommands) -> &'static str {
match analyze_cmd {
AnalyzeCommands::DeepContext { .. } => "analyze-deep-context",
AnalyzeCommands::Comprehensive { .. } => "analyze-comprehensive",
AnalyzeCommands::DefectPrediction { .. } => "analyze-defect-prediction",
AnalyzeCommands::Duplicates { .. } => "analyze-duplicates",
AnalyzeCommands::BigO { .. } => "analyze-big-o",
_ => unreachable!("Non-advanced command passed to advanced command name extractor"),
}
}
fn get_structural_analyze_command_name(analyze_cmd: &AnalyzeCommands) -> &'static str {
match analyze_cmd {
AnalyzeCommands::Dag { .. } => "analyze-dag",
AnalyzeCommands::GraphMetrics { .. } => "analyze-graph-metrics",
AnalyzeCommands::SymbolTable { .. } => "analyze-symbol-table",
AnalyzeCommands::NameSimilarity { .. } => "analyze-name-similarity",
_ => unreachable!("Non-structural command passed to structural command name extractor"),
}
}
fn get_specialized_analyze_command_name(analyze_cmd: &AnalyzeCommands) -> &'static str {
match analyze_cmd {
AnalyzeCommands::Makefile { .. } => "analyze-makefile",
AnalyzeCommands::Provability { .. } => "analyze-provability",
AnalyzeCommands::ProofAnnotations { .. } => "analyze-proof-annotations",
AnalyzeCommands::IncrementalCoverage { .. } => "analyze-incremental-coverage",
AnalyzeCommands::AssemblyScript { .. } => "analyze-assemblyscript",
AnalyzeCommands::WebAssembly { .. } => "analyze-webassembly",
_ => {
unreachable!("Non-specialized command passed to specialized command name extractor")
}
}
}
#[must_use]
pub fn from_commands(command: Commands) -> Self {
let command_name = Self::get_command_name_by_category(&command);
Self {
command,
command_name,
raw_args: std::env::args().collect(),
}
}
fn get_command_name_by_category(command: &Commands) -> String {
match command {
Commands::Analyze(analyze_cmd) => Self::get_analyze_command_name(analyze_cmd),
Commands::Qdd(qdd_cmd) => Self::get_qdd_command_name(qdd_cmd),
_ => Self::get_simple_command_name(command),
}
.to_string()
}
fn get_simple_command_name(command: &Commands) -> &'static str {
let category = Self::get_command_category(command);
match category {
CommandCategory::Generation => Self::get_generation_command_name(command),
CommandCategory::Analysis => Self::get_analysis_command_name(command),
CommandCategory::Operations => Self::get_operations_command_name(command),
CommandCategory::Workflow => Self::get_workflow_command_name(command),
CommandCategory::System => Self::get_system_command_name(command),
CommandCategory::Configuration => Self::get_configuration_command_name(command),
CommandCategory::Demo => "demo",
CommandCategory::Enforcement => "enforce",
}
}
fn get_command_category(command: &Commands) -> CommandCategory {
match command {
Commands::Generate { .. } | Commands::Scaffold { .. } => CommandCategory::Generation,
Commands::QualityGate { .. } | Commands::Report { .. } => CommandCategory::Analysis,
Commands::Serve { .. }
| Commands::Cache { .. }
| Commands::Memory { .. }
| Commands::Telemetry { .. } => CommandCategory::Operations,
Commands::Refactor(_)
| Commands::Test { .. }
| Commands::Roadmap(_)
| Commands::Validate { .. } => CommandCategory::Workflow,
Commands::List { .. }
| Commands::Search { .. }
| Commands::Context { .. }
| Commands::Diagnose(_) => CommandCategory::System,
Commands::Config { .. } | Commands::Agent { .. } | Commands::Tdg { .. } => {
CommandCategory::Configuration
}
Commands::Demo { .. } => CommandCategory::Demo,
Commands::Enforce(_) => CommandCategory::Enforcement,
Commands::Analyze(_) => {
unreachable!("Analyze commands handled by get_analyze_command_name")
}
Commands::Qdd(_) => {
unreachable!("QDD commands handled by get_qdd_command_name")
}
}
}
fn get_generation_command_name(command: &Commands) -> &'static str {
match command {
Commands::Generate { .. } => "generate",
Commands::Scaffold { .. } => "scaffold",
_ => unreachable!("Non-generation command passed to generation command name extractor"),
}
}
fn get_analysis_command_name(command: &Commands) -> &'static str {
match command {
Commands::QualityGate { .. } => "quality-gate",
Commands::Report { .. } => "report",
_ => unreachable!("Non-analysis command passed to analysis command name extractor"),
}
}
fn get_operations_command_name(command: &Commands) -> &'static str {
match command {
Commands::Serve { .. } => "serve",
Commands::Cache { .. } => "cache",
Commands::Memory { .. } => "memory",
Commands::Telemetry { .. } => "telemetry",
_ => unreachable!("Non-operations command passed to operations command name extractor"),
}
}
fn get_workflow_command_name(command: &Commands) -> &'static str {
match command {
Commands::Refactor(_) => "refactor",
Commands::Test { .. } => "test",
Commands::Roadmap(_) => "roadmap",
Commands::Validate { .. } => "validate",
_ => unreachable!("Non-workflow command passed to workflow command name extractor"),
}
}
fn get_system_command_name(command: &Commands) -> &'static str {
match command {
Commands::List { .. } => "list",
Commands::Search { .. } => "search",
Commands::Context { .. } => "context",
Commands::Diagnose(_) => "diagnose",
_ => unreachable!("Non-system command passed to system command name extractor"),
}
}
fn get_configuration_command_name(command: &Commands) -> &'static str {
match command {
Commands::Config { .. } => "config",
Commands::Agent { .. } => "agent",
Commands::Tdg { .. } => "tdg",
_ => unreachable!(
"Non-configuration command passed to configuration command name extractor"
),
}
}
}
#[derive(Debug, Clone, Copy)]
enum AnalyzeCommandCategory {
Basic,
Advanced,
Structural,
Specialized,
}
#[derive(Debug, Clone, Copy)]
enum CommandCategory {
Generation,
Analysis,
Operations,
Workflow,
System,
Configuration,
Demo,
Enforcement,
}
impl CliAdapter {
fn get_analyze_command_category(analyze_cmd: &AnalyzeCommands) -> AnalyzeCommandCategory {
match analyze_cmd {
AnalyzeCommands::Churn { .. }
| AnalyzeCommands::Complexity { .. }
| AnalyzeCommands::DeadCode { .. }
| AnalyzeCommands::Satd { .. }
| AnalyzeCommands::Tdg { .. }
| AnalyzeCommands::LintHotspot { .. }
| AnalyzeCommands::Clippy { .. }
| AnalyzeCommands::Entropy { .. } => AnalyzeCommandCategory::Basic,
AnalyzeCommands::DeepContext { .. }
| AnalyzeCommands::Comprehensive { .. }
| AnalyzeCommands::DefectPrediction { .. }
| AnalyzeCommands::Duplicates { .. }
| AnalyzeCommands::BigO { .. } => AnalyzeCommandCategory::Advanced,
AnalyzeCommands::Dag { .. }
| AnalyzeCommands::GraphMetrics { .. }
| AnalyzeCommands::SymbolTable { .. }
| AnalyzeCommands::NameSimilarity { .. } => AnalyzeCommandCategory::Structural,
AnalyzeCommands::Makefile { .. }
| AnalyzeCommands::Provability { .. }
| AnalyzeCommands::ProofAnnotations { .. }
| AnalyzeCommands::IncrementalCoverage { .. }
| AnalyzeCommands::AssemblyScript { .. }
| AnalyzeCommands::WebAssembly { .. }
| AnalyzeCommands::Wasm { .. } => AnalyzeCommandCategory::Specialized,
}
}
}
#[derive(Debug)]
pub enum CliOutput {
Success { content: String, exit_code: i32 },
Error { message: String, exit_code: i32 },
}
impl CliOutput {
pub fn write_and_exit(self) -> ! {
match self {
CliOutput::Success { content, exit_code } => {
print!("{content}");
std::process::exit(exit_code);
}
CliOutput::Error { message, exit_code } => {
eprintln!("Error: {message}");
std::process::exit(exit_code);
}
}
}
#[must_use]
pub fn exit_code(&self) -> i32 {
match self {
CliOutput::Success { exit_code, .. } => *exit_code,
CliOutput::Error { exit_code, .. } => *exit_code,
}
}
#[must_use]
pub fn content(&self) -> &str {
match self {
CliOutput::Success { content, .. } => content,
CliOutput::Error { message, .. } => message,
}
}
}
fn format_to_string(format: &ContextFormat) -> String {
match format {
ContextFormat::Markdown => "markdown".to_string(),
ContextFormat::Json => "json".to_string(),
ContextFormat::Sarif => "sarif".to_string(),
ContextFormat::LlmOptimized => "llm-optimized".to_string(),
}
}
fn churn_format_to_string(format: &ChurnOutputFormat) -> String {
match format {
ChurnOutputFormat::Summary => "summary".to_string(),
ChurnOutputFormat::Markdown => "markdown".to_string(),
ChurnOutputFormat::Json => "json".to_string(),
ChurnOutputFormat::Csv => "csv".to_string(),
}
}
fn complexity_format_to_string(format: &ComplexityOutputFormat) -> String {
match format {
ComplexityOutputFormat::Summary => "summary".to_string(),
ComplexityOutputFormat::Full => "full".to_string(),
ComplexityOutputFormat::Json => "json".to_string(),
ComplexityOutputFormat::Sarif => "sarif".to_string(),
}
}
fn dag_type_to_string(dag_type: &DagType) -> String {
match dag_type {
DagType::CallGraph => "call-graph".to_string(),
DagType::ImportGraph => "import-graph".to_string(),
DagType::Inheritance => "inheritance".to_string(),
DagType::FullDependency => "full-dependency".to_string(),
}
}
fn dead_code_format_to_string(format: &crate::cli::DeadCodeOutputFormat) -> String {
match format {
crate::cli::DeadCodeOutputFormat::Summary => "summary".to_string(),
crate::cli::DeadCodeOutputFormat::Json => "json".to_string(),
crate::cli::DeadCodeOutputFormat::Sarif => "sarif".to_string(),
crate::cli::DeadCodeOutputFormat::Markdown => "markdown".to_string(),
}
}
fn satd_format_to_string(format: &crate::cli::SatdOutputFormat) -> String {
match format {
crate::cli::SatdOutputFormat::Summary => "summary".to_string(),
crate::cli::SatdOutputFormat::Json => "json".to_string(),
crate::cli::SatdOutputFormat::Sarif => "sarif".to_string(),
crate::cli::SatdOutputFormat::Markdown => "markdown".to_string(),
}
}
fn satd_severity_to_string(severity: &crate::cli::SatdSeverity) -> String {
match severity {
crate::cli::SatdSeverity::Critical => "critical".to_string(),
crate::cli::SatdSeverity::High => "high".to_string(),
crate::cli::SatdSeverity::Medium => "medium".to_string(),
crate::cli::SatdSeverity::Low => "low".to_string(),
}
}
fn graph_metric_type_to_string(metric: &crate::cli::GraphMetricType) -> String {
match metric {
crate::cli::GraphMetricType::All => "all".to_string(),
crate::cli::GraphMetricType::Centrality => "centrality".to_string(),
crate::cli::GraphMetricType::Betweenness => "betweenness".to_string(),
crate::cli::GraphMetricType::Closeness => "closeness".to_string(),
crate::cli::GraphMetricType::PageRank => "pagerank".to_string(),
crate::cli::GraphMetricType::Clustering => "clustering".to_string(),
crate::cli::GraphMetricType::Components => "components".to_string(),
}
}
fn graph_metrics_format_to_string(format: &crate::cli::GraphMetricsOutputFormat) -> String {
match format {
crate::cli::GraphMetricsOutputFormat::Summary => "summary".to_string(),
crate::cli::GraphMetricsOutputFormat::Detailed => "detailed".to_string(),
crate::cli::GraphMetricsOutputFormat::Human => "human".to_string(),
crate::cli::GraphMetricsOutputFormat::Json => "json".to_string(),
crate::cli::GraphMetricsOutputFormat::Csv => "csv".to_string(),
crate::cli::GraphMetricsOutputFormat::GraphML => "graphml".to_string(),
crate::cli::GraphMetricsOutputFormat::Markdown => "markdown".to_string(),
}
}
fn name_similarity_format_to_string(format: &crate::cli::NameSimilarityOutputFormat) -> String {
match format {
crate::cli::NameSimilarityOutputFormat::Summary => "summary".to_string(),
crate::cli::NameSimilarityOutputFormat::Detailed => "detailed".to_string(),
crate::cli::NameSimilarityOutputFormat::Human => "human".to_string(),
crate::cli::NameSimilarityOutputFormat::Json => "json".to_string(),
crate::cli::NameSimilarityOutputFormat::Csv => "csv".to_string(),
crate::cli::NameSimilarityOutputFormat::Markdown => "markdown".to_string(),
}
}
fn property_type_filter_to_string(filter: &crate::cli::PropertyTypeFilter) -> String {
match filter {
crate::cli::PropertyTypeFilter::All => "all".to_string(),
crate::cli::PropertyTypeFilter::MemorySafety => "memory-safety".to_string(),
crate::cli::PropertyTypeFilter::ThreadSafety => "thread-safety".to_string(),
crate::cli::PropertyTypeFilter::DataRaceFreeze => "data-race-freeze".to_string(),
crate::cli::PropertyTypeFilter::Termination => "termination".to_string(),
crate::cli::PropertyTypeFilter::FunctionalCorrectness => {
"functional-correctness".to_string()
}
crate::cli::PropertyTypeFilter::ResourceBounds => "resource-bounds".to_string(),
}
}
fn verification_method_filter_to_string(method: &crate::cli::VerificationMethodFilter) -> String {
match method {
crate::cli::VerificationMethodFilter::All => "all".to_string(),
crate::cli::VerificationMethodFilter::FormalProof => "formal-proof".to_string(),
crate::cli::VerificationMethodFilter::ModelChecking => "model-checking".to_string(),
crate::cli::VerificationMethodFilter::StaticAnalysis => "static-analysis".to_string(),
crate::cli::VerificationMethodFilter::AbstractInterpretation => {
"abstract-interpretation".to_string()
}
crate::cli::VerificationMethodFilter::BorrowChecker => "borrow-checker".to_string(),
}
}
fn proof_annotation_format_to_string(format: &crate::cli::ProofAnnotationOutputFormat) -> String {
match format {
crate::cli::ProofAnnotationOutputFormat::Summary => "summary".to_string(),
crate::cli::ProofAnnotationOutputFormat::Full => "full".to_string(),
crate::cli::ProofAnnotationOutputFormat::Json => "json".to_string(),
crate::cli::ProofAnnotationOutputFormat::Markdown => "markdown".to_string(),
crate::cli::ProofAnnotationOutputFormat::Sarif => "sarif".to_string(),
}
}
fn incremental_coverage_format_to_string(
format: &crate::cli::IncrementalCoverageOutputFormat,
) -> String {
match format {
crate::cli::IncrementalCoverageOutputFormat::Summary => "summary".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Detailed => "detailed".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Json => "json".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Markdown => "markdown".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Lcov => "lcov".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Delta => "delta".to_string(),
crate::cli::IncrementalCoverageOutputFormat::Sarif => "sarif".to_string(),
}
}
fn symbol_type_filter_to_string(filter: &crate::cli::SymbolTypeFilter) -> String {
match filter {
crate::cli::SymbolTypeFilter::All => "all".to_string(),
crate::cli::SymbolTypeFilter::Functions => "functions".to_string(),
crate::cli::SymbolTypeFilter::Classes => "classes".to_string(),
crate::cli::SymbolTypeFilter::Types => "types".to_string(),
crate::cli::SymbolTypeFilter::Variables => "variables".to_string(),
crate::cli::SymbolTypeFilter::Modules => "modules".to_string(),
}
}
fn symbol_table_format_to_string(format: &crate::cli::SymbolTableOutputFormat) -> String {
match format {
crate::cli::SymbolTableOutputFormat::Summary => "summary".to_string(),
crate::cli::SymbolTableOutputFormat::Detailed => "detailed".to_string(),
crate::cli::SymbolTableOutputFormat::Human => "human".to_string(),
crate::cli::SymbolTableOutputFormat::Json => "json".to_string(),
crate::cli::SymbolTableOutputFormat::Csv => "csv".to_string(),
}
}
fn big_o_format_to_string(format: &crate::cli::BigOOutputFormat) -> String {
match format {
crate::cli::BigOOutputFormat::Summary => "summary".to_string(),
crate::cli::BigOOutputFormat::Json => "json".to_string(),
crate::cli::BigOOutputFormat::Markdown => "markdown".to_string(),
crate::cli::BigOOutputFormat::Detailed => "detailed".to_string(),
}
}
pub struct CliRunner {
adapter: CliAdapter,
}
impl CliRunner {
#[must_use]
pub fn new() -> Self {
Self {
adapter: CliAdapter::new(),
}
}
pub async fn run_command<H>(
&self,
command: Commands,
handler: H,
) -> Result<CliOutput, ProtocolError>
where
H: Fn(
UnifiedRequest,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<UnifiedResponse, ProtocolError>> + Send>,
>,
{
let input = CliInput::from_commands(command);
let unified_request = self.adapter.decode(input).await?;
let unified_response = handler(unified_request).await?;
self.adapter.encode(unified_response).await
}
}
impl Default for CliRunner {
fn default() -> Self {
Self::new()
}
}
fn deep_context_format_to_string(format: &crate::cli::DeepContextOutputFormat) -> String {
match format {
crate::cli::DeepContextOutputFormat::Markdown => "markdown".to_string(),
crate::cli::DeepContextOutputFormat::Json => "json".to_string(),
crate::cli::DeepContextOutputFormat::Sarif => "sarif".to_string(),
}
}
fn deep_context_dag_type_to_string(dag_type: &crate::cli::DeepContextDagType) -> String {
match dag_type {
crate::cli::DeepContextDagType::CallGraph => "call-graph".to_string(),
crate::cli::DeepContextDagType::ImportGraph => "import-graph".to_string(),
crate::cli::DeepContextDagType::Inheritance => "inheritance".to_string(),
crate::cli::DeepContextDagType::FullDependency => "full-dependency".to_string(),
}
}
fn deep_context_cache_strategy_to_string(
strategy: &crate::cli::DeepContextCacheStrategy,
) -> String {
match strategy {
crate::cli::DeepContextCacheStrategy::Normal => "normal".to_string(),
crate::cli::DeepContextCacheStrategy::ForceRefresh => "force-refresh".to_string(),
crate::cli::DeepContextCacheStrategy::Offline => "offline".to_string(),
}
}
fn tdg_format_to_string(format: &crate::cli::TdgOutputFormat) -> String {
match format {
crate::cli::TdgOutputFormat::Table => "table".to_string(),
crate::cli::TdgOutputFormat::Json => "json".to_string(),
crate::cli::TdgOutputFormat::Markdown => "markdown".to_string(),
crate::cli::TdgOutputFormat::Sarif => "sarif".to_string(),
}
}
fn provability_format_to_string(format: &crate::cli::ProvabilityOutputFormat) -> String {
match format {
crate::cli::ProvabilityOutputFormat::Summary => "summary".to_string(),
crate::cli::ProvabilityOutputFormat::Full => "full".to_string(),
crate::cli::ProvabilityOutputFormat::Json => "json".to_string(),
crate::cli::ProvabilityOutputFormat::Sarif => "sarif".to_string(),
crate::cli::ProvabilityOutputFormat::Markdown => "markdown".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::commands::ServeTransport;
use crate::cli::{
AnalyzeCommands, Commands, ComplexityOutputFormat, DagType, DeadCodeOutputFormat,
OutputFormat, SatdOutputFormat,
};
use crate::cli::{
DeepContextCacheStrategy, DeepContextDagType, DeepContextOutputFormat, DemoProtocol,
};
use crate::models::churn::ChurnOutputFormat;
use crate::unified_protocol::{HeaderMap, StatusCode, Uuid};
use serde_json::{json, Value};
use std::path::PathBuf;
#[test]
fn test_cli_input_creation() {
let params = vec![
(
"project_name".to_string(),
Value::String("test".to_string()),
),
("version".to_string(), Value::String("1.0.0".to_string())),
];
let command = Commands::Generate {
category: "makefile".to_string(),
template: "rust/cli".to_string(),
params,
output: Some(PathBuf::from("Makefile")),
create_dirs: true,
};
let input = CliInput::from_commands(command);
assert_eq!(input.command_name, "generate");
}
#[tokio::test]
async fn test_cli_adapter_decode_generate() {
let adapter = CliAdapter::new();
let params = vec![(
"project_name".to_string(),
Value::String("test".to_string()),
)];
let command = Commands::Generate {
category: "makefile".to_string(),
template: "rust/cli".to_string(),
params,
output: None,
create_dirs: false,
};
let input = CliInput::from_commands(command);
let unified_request = adapter.decode(input).await.unwrap();
assert_eq!(unified_request.method, Method::POST);
assert_eq!(unified_request.path, "/api/v1/generate");
assert_eq!(
unified_request.get_extension::<Protocol>("protocol"),
Some(Protocol::Cli)
);
let cli_context: CliContext = unified_request.get_extension("cli_context").unwrap();
assert_eq!(cli_context.command, "generate");
}
#[tokio::test]
async fn test_cli_adapter_decode_list() {
let adapter = CliAdapter::new();
let command = Commands::List {
toolchain: Some("rust".to_string()),
category: None,
format: OutputFormat::Json,
};
let input = CliInput::from_commands(command);
let unified_request = adapter.decode(input).await.unwrap();
assert_eq!(unified_request.method, Method::GET);
assert!(unified_request.path.starts_with("/api/v1/templates"));
assert!(unified_request.path.contains("toolchain=rust"));
}
#[tokio::test]
async fn test_cli_adapter_decode_analyze_complexity() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::Complexity {
path: PathBuf::from("."),
project_path: None,
file: None,
files: vec![],
toolchain: Some("rust".to_string()),
format: ComplexityOutputFormat::Json,
output: None,
max_cyclomatic: Some(10),
max_cognitive: Some(15),
include: vec!["**/*.rs".to_string()],
watch: false,
top_files: 0,
fail_on_violation: false,
timeout: 60,
});
let input = CliInput::from_commands(command);
let unified_request = adapter.decode(input).await.unwrap();
assert_eq!(unified_request.method, Method::POST);
assert_eq!(unified_request.path, "/api/v1/analyze/complexity");
let body_bytes = axum::body::to_bytes(unified_request.body, usize::MAX)
.await
.unwrap();
let body_json: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(body_json["toolchain"], "rust");
assert_eq!(body_json["max_cyclomatic"], 10);
assert_eq!(body_json["max_cognitive"], 15);
}
#[tokio::test]
async fn test_cli_adapter_encode_success() {
let adapter = CliAdapter::new();
let response = UnifiedResponse::ok()
.with_json(&json!({"message": "success"}))
.unwrap();
let output = adapter.encode(response).await.unwrap();
match output {
CliOutput::Success { content, exit_code } => {
assert_eq!(exit_code, 0);
assert!(content.contains("success"));
}
_ => panic!("Expected success output"),
}
}
#[tokio::test]
async fn test_cli_adapter_encode_error() {
let adapter = CliAdapter::new();
let response = UnifiedResponse::new(axum::http::StatusCode::BAD_REQUEST)
.with_json(&json!({"error": "Invalid request"}))
.unwrap();
let output = adapter.encode(response).await.unwrap();
match output {
CliOutput::Error { message, exit_code } => {
assert_eq!(exit_code, 1);
assert!(message.contains("Invalid request"));
}
_ => panic!("Expected error output"),
}
}
#[test]
fn test_format_conversions() {
assert_eq!(format_to_string(&ContextFormat::Markdown), "markdown");
assert_eq!(format_to_string(&ContextFormat::Json), "json");
assert_eq!(
churn_format_to_string(&ChurnOutputFormat::Summary),
"summary"
);
assert_eq!(churn_format_to_string(&ChurnOutputFormat::Json), "json");
assert_eq!(
complexity_format_to_string(&ComplexityOutputFormat::Sarif),
"sarif"
);
assert_eq!(dag_type_to_string(&DagType::CallGraph), "call-graph");
assert_eq!(
dag_type_to_string(&DagType::FullDependency),
"full-dependency"
);
}
#[test]
fn test_cli_output_methods() {
let success = CliOutput::Success {
content: "test content".to_string(),
exit_code: 0,
};
assert_eq!(success.exit_code(), 0);
assert_eq!(success.content(), "test content");
let error = CliOutput::Error {
message: "test error".to_string(),
exit_code: 1,
};
assert_eq!(error.exit_code(), 1);
assert_eq!(error.content(), "test error");
}
#[test]
fn test_cli_adapter_new() {
let adapter = CliAdapter::new();
assert!(std::mem::size_of_val(&adapter) >= 0);
}
#[test]
fn test_cli_adapter_default() {
let adapter = CliAdapter::default();
assert!(std::mem::size_of_val(&adapter) >= 0);
}
#[tokio::test]
async fn test_decode_scaffold_project() {
let adapter = CliAdapter::new();
let params = vec![(
"project_name".to_string(),
Value::String("test_project".to_string()),
)];
let command = Commands::Scaffold {
command: ScaffoldCommands::Project {
toolchain: "rust".to_string(),
templates: vec!["cli".to_string()],
params,
parallel: 4,
},
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/scaffold");
}
#[tokio::test]
async fn test_decode_search() {
let adapter = CliAdapter::new();
let command = Commands::Search {
query: "rust cli".to_string(),
toolchain: Some("rust".to_string()),
limit: 10,
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::GET);
assert!(request.path.starts_with("/api/v1/search"));
assert!(request.path.contains("query=rust%20cli"));
assert!(request.path.contains("limit=10"));
}
#[tokio::test]
async fn test_decode_validate() {
let adapter = CliAdapter::new();
let params = vec![("key".to_string(), Value::String("value".to_string()))];
let command = Commands::Validate {
uri: "template://rust/cli".to_string(),
params,
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/validate");
}
#[tokio::test]
async fn test_decode_context() {
let adapter = CliAdapter::new();
let command = Commands::Context {
toolchain: Some("rust".to_string()),
project_path: PathBuf::from("/test/project"),
output: Some(PathBuf::from("context.md")),
format: ContextFormat::Markdown,
include_large_files: false,
skip_expensive_metrics: true,
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/context");
}
#[tokio::test]
async fn test_decode_analyze_churn() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::Churn {
project_path: PathBuf::from("."),
days: 30,
format: ChurnOutputFormat::Json,
output: None,
top_files: 10,
include: vec![],
exclude: vec![],
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/churn");
}
#[tokio::test]
async fn test_decode_analyze_dag() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::Dag {
dag_type: DagType::FullDependency,
project_path: PathBuf::from("."),
output: None,
max_depth: Some(5),
target_nodes: None,
filter_external: false,
show_complexity: false,
include_duplicates: false,
enhanced: false,
include_dead_code: false,
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/dag");
}
#[tokio::test]
async fn test_decode_analyze_dead_code() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::DeadCode {
path: PathBuf::from("."),
format: DeadCodeOutputFormat::Json,
top_files: Some(10),
include_unreachable: true,
min_dead_lines: 10,
include_tests: false,
output: None,
fail_on_violation: false,
max_percentage: 15.0,
timeout: 60,
include: vec![],
exclude: vec![],
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/dead-code");
}
#[tokio::test]
async fn test_decode_analyze_satd() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::Satd {
path: PathBuf::from("."),
format: SatdOutputFormat::Json,
severity: None,
critical_only: false,
include_tests: false,
strict: false,
evolution: false,
days: 30,
metrics: false,
output: None,
top_files: 10,
fail_on_violation: false,
timeout: 60,
include: vec![],
exclude: vec![],
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/satd");
}
#[tokio::test]
async fn test_decode_demo() {
let adapter = CliAdapter::new();
let command = Commands::Demo {
path: Some(PathBuf::from(".")),
url: None,
repo: None,
format: OutputFormat::Table,
protocol: DemoProtocol::Http,
show_api: false,
no_browser: false,
port: Some(8080),
cli: true,
target_nodes: 100,
centrality_threshold: 0.5,
merge_threshold: 3,
debug: false,
debug_output: None,
skip_vendor: true,
no_skip_vendor: false,
max_line_length: None,
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/demo");
}
#[tokio::test]
async fn test_decode_serve() {
let adapter = CliAdapter::new();
let command = Commands::Serve {
host: "127.0.0.1".to_string(),
port: 3000,
cors: true,
transport: ServeTransport::Http,
};
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/serve");
}
#[test]
fn test_cli_input_from_commands_generate() {
let params = vec![(
"project_name".to_string(),
Value::String("test".to_string()),
)];
let command = Commands::Generate {
category: "makefile".to_string(),
template: "rust/cli".to_string(),
params,
output: Some(PathBuf::from("output.txt")),
create_dirs: true,
};
let input = CliInput::from_commands(command);
assert_eq!(input.command_name, "generate");
assert!(input.raw_args.len() >= 0); }
#[test]
fn test_cli_input_from_commands_list() {
let command = Commands::List {
toolchain: Some("rust".to_string()),
category: Some("cli".to_string()),
format: OutputFormat::Json,
};
let input = CliInput::from_commands(command);
assert_eq!(input.command_name, "list");
assert_eq!(input.command_name, "list");
}
#[test]
fn test_cli_output_success() {
let output = CliOutput::Success {
content: "Success message".to_string(),
exit_code: 0,
};
assert_eq!(output.exit_code(), 0);
assert_eq!(output.content(), "Success message");
}
#[test]
fn test_cli_output_error() {
let output = CliOutput::Error {
message: "Error occurred".to_string(),
exit_code: 2,
};
assert_eq!(output.exit_code(), 2);
assert_eq!(output.content(), "Error occurred");
}
#[test]
fn test_cli_runner_new() {
let runner = CliRunner::new();
assert!(std::mem::size_of_val(&runner) >= 0);
}
#[test]
fn test_cli_runner_default() {
let runner = CliRunner::default();
assert!(std::mem::size_of_val(&runner) >= 0);
}
#[tokio::test]
async fn test_unsupported_command() {
let adapter = CliAdapter::new();
let diagnose_args = crate::cli::diagnose::DiagnoseArgs {
format: crate::cli::diagnose::DiagnosticFormat::Pretty,
only: vec![],
skip: vec![],
timeout: 60,
};
let command = Commands::Diagnose(diagnose_args);
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_err());
match result {
Err(ProtocolError::UnsupportedProtocol(_)) => {}
_ => panic!("Expected UnsupportedProtocol error"),
}
}
#[tokio::test]
async fn test_decode_analyze_deep_context() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::DeepContext {
project_path: PathBuf::from("."),
output: Some(PathBuf::from("deep_context.json")),
format: DeepContextOutputFormat::Json,
full: false,
include: vec![],
exclude: vec![],
period_days: 30,
dag_type: DeepContextDagType::CallGraph,
max_depth: None,
include_patterns: vec![],
exclude_patterns: vec![],
cache_strategy: DeepContextCacheStrategy::Normal,
parallel: None,
verbose: false,
top_files: 10,
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/deep-context");
}
#[tokio::test]
async fn test_decode_analyze_tdg() {
let adapter = CliAdapter::new();
let command = Commands::Analyze(AnalyzeCommands::Tdg {
path: PathBuf::from("."),
threshold: 1.5,
top_files: 10,
format: crate::cli::TdgOutputFormat::Json,
include_components: false,
output: None,
critical_only: false,
verbose: false,
});
let input = CliInput::from_commands(command);
let result = adapter.decode(input).await;
assert!(result.is_ok());
let request = result.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.path, "/api/v1/analyze/tdg");
}
#[tokio::test]
async fn test_encode_success_response() {
let adapter = CliAdapter::new();
let response = UnifiedResponse {
status: StatusCode::OK,
headers: HeaderMap::new(),
body: Body::from(json!({"status": "success"}).to_string()),
trace_id: Uuid::new_v4(),
};
let result = adapter.encode(response).await;
assert!(result.is_ok());
let cli_output = result.unwrap();
match cli_output {
CliOutput::Success { content, .. } => {
assert!(content.contains("success"));
}
_ => panic!("Expected Success output"),
}
}
#[tokio::test]
async fn test_encode_error_response() {
let adapter = CliAdapter::new();
let response = UnifiedResponse {
status: StatusCode::BAD_REQUEST,
headers: HeaderMap::new(),
body: Body::from("Bad Request"),
trace_id: Uuid::new_v4(),
};
let result = adapter.encode(response).await;
assert!(result.is_ok());
let cli_output = result.unwrap();
match cli_output {
CliOutput::Error { message, exit_code } => {
assert!(message.contains("Bad Request"));
assert_eq!(exit_code, 1);
}
_ => panic!("Expected Error output"),
}
}
#[tokio::test]
async fn test_dispatch_basic_analysis_churn() {
let command = AnalyzeCommands::Churn {
project_path: PathBuf::from("."),
days: 30,
format: ChurnOutputFormat::Json,
output: None,
top_files: 10,
include: vec![],
exclude: vec![],
};
let result = CliAdapter::dispatch_basic_analysis(&command);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/churn");
}
#[tokio::test]
async fn test_dispatch_basic_analysis_complexity() {
let command = AnalyzeCommands::Complexity {
path: PathBuf::from("."),
project_path: None,
file: None,
files: vec![],
toolchain: Some("rust".to_string()),
format: ComplexityOutputFormat::Json,
output: None,
max_cyclomatic: Some(10),
max_cognitive: Some(15),
include: vec![],
watch: false,
top_files: 0,
fail_on_violation: false,
timeout: 60,
};
let result = CliAdapter::dispatch_basic_analysis(&command);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/complexity");
}
#[tokio::test]
async fn test_dispatch_advanced_analysis_comprehensive() {
let command = AnalyzeCommands::Comprehensive {
project_path: PathBuf::from("."),
file: None,
files: vec![],
format: crate::cli::ComprehensiveOutputFormat::Json,
include_duplicates: true,
include_dead_code: true,
include_defects: true,
include_complexity: true,
include_tdg: false,
confidence_threshold: 0.8,
min_lines: 10,
include: None,
exclude: None,
output: None,
perf: false,
executive_summary: false,
top_files: 10,
};
let result = CliAdapter::dispatch_advanced_analysis(&command);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/comprehensive");
}
#[tokio::test]
async fn test_dispatch_structural_analysis_dag() {
let command = AnalyzeCommands::Dag {
dag_type: DagType::CallGraph,
project_path: PathBuf::from("."),
output: None,
max_depth: Some(5),
target_nodes: Some(100),
filter_external: true,
show_complexity: false,
include_duplicates: false,
include_dead_code: false,
enhanced: false,
};
let result = CliAdapter::dispatch_structural_analysis(&command);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/dag");
}
#[tokio::test]
async fn test_dispatch_specialized_analysis_makefile() {
let command = AnalyzeCommands::Makefile {
path: PathBuf::from("."),
rules: vec!["all".to_string()],
format: crate::cli::MakefileOutputFormat::Json,
fix: false,
gnu_version: "4.3".to_string(),
top_files: 5,
};
let result = CliAdapter::dispatch_specialized_analysis(&command);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/makefile");
}
#[test]
fn test_decode_analyze_complexity_with_migration_new_path() {
let result = CliAdapter::decode_analyze_complexity_with_migration(
&PathBuf::from("."),
&None, &None,
&[],
&Some("rust".to_string()),
&ComplexityOutputFormat::Json,
&None,
&Some(10),
&Some(15),
&[],
false,
0,
);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/complexity");
}
#[test]
fn test_decode_analyze_complexity_with_migration_deprecated_path() {
let result = CliAdapter::decode_analyze_complexity_with_migration(
&PathBuf::from("new_path"),
&Some(PathBuf::from("deprecated_path")), &None,
&[],
&Some("rust".to_string()),
&ComplexityOutputFormat::Json,
&None,
&Some(10),
&Some(15),
&[],
false,
0,
);
assert!(result.is_ok());
let (method, path, _, _) = result.unwrap();
assert_eq!(method, Method::POST);
assert_eq!(path, "/api/v1/analyze/complexity");
}
#[test]
fn test_dispatch_wrong_category_returns_error() {
let churn_command = AnalyzeCommands::Churn {
project_path: PathBuf::from("."),
days: 30,
format: ChurnOutputFormat::Json,
output: None,
top_files: 10,
include: vec![],
exclude: vec![],
};
let result = CliAdapter::dispatch_advanced_analysis(&churn_command);
assert!(result.is_err());
match result {
Err(ProtocolError::UnsupportedProtocol(msg)) => {
assert!(msg.contains("Command not supported in advanced analysis dispatch"));
}
_ => panic!("Expected UnsupportedProtocol error"),
}
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}