use crate::commands::{AdaptersCommands, DagCli};
use crate::routes::preconditions::require_file;
use crate::{check_engine, emit_json, parse_graph, read_file, ExitCode};
use bijux_dag_runtime::{
adapter_admission_matrix, adapter_conformance_suite, adapter_registry_dump,
generate_adapter_reference_markdown, probe_external_adapters, registered_adapter_descriptors,
registered_adapter_reference_document, registered_adapters,
validate_output_schema_compatibility, CacheCompatibilityMode,
};
use serde_json::json;
use std::path::Path;
pub(crate) fn handle_adapters_command(
cli: &DagCli,
command: &AdaptersCommands,
) -> Result<ExitCode, ExitCode> {
match command {
AdaptersCommands::Ls => {
let adapters = registered_adapters();
if cli.json {
return emit_json(
cli,
"dag.adapters.ls",
true,
json!(adapters),
Vec::new(),
ExitCode::SUCCESS,
);
}
for adapter in adapters {
println!(
"{} {} effects={:?}",
adapter.adapter_id, adapter.adapter_version, adapter.effects
);
}
Ok(ExitCode::SUCCESS)
}
AdaptersCommands::Dump => {
let data = json!({
"registry": adapter_registry_dump(),
"descriptors": registered_adapter_descriptors(),
"external_handshakes": probe_external_adapters().map_err(|_| ExitCode::from(3))?,
});
if cli.json {
return emit_json(
cli,
"dag.adapters.dump",
true,
data,
Vec::new(),
ExitCode::SUCCESS,
);
}
println!("{}", serde_json::to_string_pretty(&data).map_err(|_| ExitCode::from(3))?);
Ok(ExitCode::SUCCESS)
}
AdaptersCommands::Describe => {
let descriptors = registered_adapter_descriptors();
if cli.json {
return emit_json(
cli,
"dag.adapters.describe",
true,
json!({ "descriptors": descriptors }),
Vec::new(),
ExitCode::SUCCESS,
);
}
for descriptor in descriptors {
println!(
"{} {} kinds={:?} effects={:?} timeout={} cancel={} cache={:?}",
descriptor.id,
descriptor.version,
descriptor.supported_kinds,
descriptor.required_effects,
descriptor.supports_timeout,
descriptor.supports_cancel,
descriptor.cache_compatibility,
);
}
Ok(ExitCode::SUCCESS)
}
AdaptersCommands::Admit { dag } => {
require_file(dag)?;
let input = read_file(dag)?;
let graph = parse_graph(&input)?;
let report = adapter_admission_matrix(&graph);
let code = if report.supported { ExitCode::SUCCESS } else { ExitCode::from(3) };
if cli.json {
return emit_json(
cli,
"dag.adapters.admit",
report.supported,
json!(report),
Vec::new(),
code,
);
}
for entry in &report.entries {
if entry.supported {
println!("ok {} kind={}", entry.node_id, entry.node_kind);
} else {
println!(
"unsupported {} kind={} reasons={}",
entry.node_id,
entry.node_kind,
entry.reasons.join("; ")
);
}
}
if report.supported {
Ok(ExitCode::SUCCESS)
} else {
Err(ExitCode::from(3))
}
}
AdaptersCommands::Conformance => {
let suites = adapter_conformance_suite().map_err(|_| ExitCode::from(3))?;
let ok = suites.iter().all(|suite| {
suite.scenarios.iter().all(|scenario| {
!matches!(scenario.status, bijux_dag_runtime::AdapterScenarioStatus::Fail)
})
});
if cli.json {
return emit_json(
cli,
"dag.adapters.conformance",
ok,
json!({ "suites": suites }),
Vec::new(),
if ok { ExitCode::SUCCESS } else { ExitCode::from(3) },
);
}
for suite in suites {
println!("{} {}", suite.adapter_id, suite.adapter_version);
for scenario in suite.scenarios {
println!(" {} {:?}", scenario.scenario, scenario.status);
}
}
if ok {
Ok(ExitCode::SUCCESS)
} else {
Err(ExitCode::from(3))
}
}
AdaptersCommands::CacheCompat { meta, expected_schema } => {
require_file(meta)?;
let report = cache_compatibility_payload(meta, expected_schema)?;
let ok = report["compatible"].as_bool().unwrap_or(false);
if cli.json {
return emit_json(
cli,
"dag.adapters.cache-compat",
ok,
report,
Vec::new(),
if ok { ExitCode::SUCCESS } else { ExitCode::from(3) },
);
}
println!("{}", serde_json::to_string_pretty(&report).map_err(|_| ExitCode::from(3))?);
if ok {
Ok(ExitCode::SUCCESS)
} else {
Err(ExitCode::from(3))
}
}
AdaptersCommands::Reference => {
let document = registered_adapter_reference_document();
if cli.json {
return emit_json(
cli,
"dag.adapters.reference",
true,
json!({
"document": document,
"markdown": generate_adapter_reference_markdown(&document),
}),
Vec::new(),
ExitCode::SUCCESS,
);
}
println!("{}", generate_adapter_reference_markdown(&document));
Ok(ExitCode::SUCCESS)
}
AdaptersCommands::Doctor => {
let docker = check_engine("docker");
let podman = check_engine("podman");
let handshakes = probe_external_adapters().map_err(|_| ExitCode::from(3))?;
let descriptors = registered_adapter_descriptors();
let ok = (docker.get("status").and_then(|v| v.as_str()) == Some("ok")
|| podman.get("status").and_then(|v| v.as_str()) == Some("ok"))
&& handshakes.iter().all(|report| {
report.status == bijux_dag_runtime::ExternalAdapterHandshakeStatus::Ok
});
let payload = json!({
"docker": docker,
"podman": podman,
"descriptors": descriptors,
"external_handshakes": handshakes,
});
if cli.json {
return emit_json(
cli,
"dag.adapters.doctor",
ok,
payload,
Vec::new(),
if ok { ExitCode::SUCCESS } else { ExitCode::from(3) },
);
}
println!("docker: {}", payload["docker"]["status"]);
if let Some(version) = payload["docker"].get("version").and_then(|value| value.as_str())
{
println!("docker_version: {}", version);
}
println!("podman: {}", payload["podman"]["status"]);
if let Some(version) = payload["podman"].get("version").and_then(|value| value.as_str())
{
println!("podman_version: {}", version);
}
for report in payload["external_handshakes"].as_array().into_iter().flatten() {
println!(
"handshake {} status={}",
report["path"].as_str().unwrap_or("<unknown>"),
report["status"].as_str().unwrap_or("unknown")
);
if let Some(reason) = report.get("reason").and_then(|value| value.as_str()) {
println!("reason: {}", reason);
}
}
if ok {
Ok(ExitCode::SUCCESS)
} else {
Err(ExitCode::from(3))
}
}
}
}
fn cache_compatibility_payload(
meta: &Path,
expected_schema: &str,
) -> Result<serde_json::Value, ExitCode> {
let data = read_file(meta)?;
let meta: serde_json::Value = serde_json::from_str(&data).map_err(|_| ExitCode::from(3))?;
let adapter_id = meta.get("adapter_id").and_then(|value| value.as_str()).unwrap_or("unknown");
let adapter_version =
meta.get("adapter_version").and_then(|value| value.as_str()).unwrap_or("unknown");
let produced_schema_version = meta
.get("produces_outputs_schema_version")
.or_else(|| meta.get("output_schema_version"))
.and_then(|value| value.as_str())
.unwrap_or_default();
let compatibility_mode = registered_adapter_descriptors()
.into_iter()
.find(|descriptor| descriptor.id == adapter_id && descriptor.version == adapter_version)
.map(|descriptor| descriptor.cache_compatibility)
.unwrap_or(CacheCompatibilityMode::FingerprintExact);
let report = validate_output_schema_compatibility(
compatibility_mode,
produced_schema_version,
expected_schema,
);
Ok(json!({
"adapter_id": adapter_id,
"adapter_version": adapter_version,
"compatible": report.compatible,
"compatibility_mode": report.compatibility_mode,
"produced_schema_version": report.produced_schema_version,
"expected_schema_version": report.expected_schema_version,
"reason": report.reason,
}))
}
#[cfg(test)]
mod tests {
use super::handle_adapters_command;
use crate::commands::{AdaptersCommands, Commands, DagCli};
use std::fs;
fn cli(json: bool) -> DagCli {
DagCli {
json,
quiet: true,
command: Commands::Adapters { command: AdaptersCommands::Describe },
}
}
#[test]
fn adapter_describe_json_exposes_descriptor_contract_fields() {
let result = handle_adapters_command(&cli(true), &AdaptersCommands::Describe);
assert!(result.is_ok());
}
#[test]
fn adapter_admit_reports_missing_adapter_kind() {
let dir = tempfile::tempdir().expect("tmpdir");
let dag = dir.path().join("graph.json");
fs::write(
&dag,
r#"{
"spec":"bijux-dag/v0.1",
"nodes":[{"id":"x","kind":"missing.kind","outputs":[{"name":"out","path":"out"}],"params":{}}],
"edges":[]
}"#,
)
.expect("write graph");
let result = handle_adapters_command(&cli(true), &AdaptersCommands::Admit { dag });
assert!(result.is_err());
}
#[test]
fn adapter_cache_compat_rejects_schema_mismatch() {
let dir = tempfile::tempdir().expect("tmpdir");
let meta = dir.path().join("meta.json");
fs::write(
&meta,
r#"{
"adapter_id":"shell",
"adapter_version":"0.1",
"produces_outputs_schema_version":"schema/v1"
}"#,
)
.expect("write meta");
let result = handle_adapters_command(
&cli(true),
&AdaptersCommands::CacheCompat { meta, expected_schema: "schema/v2".to_string() },
);
assert!(result.is_err());
}
}