az-device-contract-codegen 2026.5.18

Generate deterministic Modbus C, Markdown, and Tokio client artifacts from typed device contract definitions.
Documentation
use az_device_contract_codegen::{
    generate_bundle, ApiKind, ContractMethod, ContractRenderRequest, ContractService,
    ContractTypedItem, GeneratedArtifact, GeneratedBundle, ReadReturnKind, RegisterArea,
    TransportKind, ValueType,
};
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn should_compile_generated_c_and_rust_artifacts() -> Result<(), Box<dyn Error>> {
    let request = build_sample_request();
    let bundle = generate_bundle(&request)?;
    let temp_root = create_temp_root("generated-artifacts-compile");

    write_generated_bundle(&bundle, &temp_root)?;
    compile_generated_c_sources(&temp_root)?;
    compile_generated_rust_artifact(&temp_root, &bundle, "rtu")?;
    compile_generated_rust_artifact(&temp_root, &bundle, "tcp")?;
    Ok(())
}

fn build_sample_request() -> ContractRenderRequest {
    ContractRenderRequest {
        transports: vec![TransportKind::Rtu, TransportKind::Tcp],
        services: vec![
            ContractService {
                api_kind: ApiKind::StaticRead,
                interface_name: "DeviceStaticReadApi".to_string(),
                interface_summary: Some("静态信息读取".to_string()),
                methods: vec![ContractMethod {
                    summary: Some("读取设备身份".to_string()),
                    method_name: "getDeviceIdentity".to_string(),
                    read_return_kind: Some(ReadReturnKind::Dto),
                    read_return_type_name: Some("DeviceIdentityRegisters".to_string()),
                    read_area: Some(RegisterArea::HoldingRegister),
                    write_area: None,
                    read_fields: vec![
                        ContractTypedItem {
                            name: "macAddress".to_string(),
                            summary: Some("MAC 地址".to_string()),
                            value_type: ValueType::String,
                        },
                        ContractTypedItem {
                            name: "flashCapacityKb".to_string(),
                            summary: Some("Flash 容量".to_string()),
                            value_type: ValueType::Int,
                        },
                    ],
                    parameters: vec![],
                }],
            },
            ContractService {
                api_kind: ApiKind::RuntimeRead,
                interface_name: "DeviceReadApi".to_string(),
                interface_summary: Some("运行态读取".to_string()),
                methods: vec![
                    ContractMethod {
                        summary: Some("读取信号灯".to_string()),
                        method_name: "getSignalLights".to_string(),
                        read_return_kind: Some(ReadReturnKind::Dto),
                        read_return_type_name: Some("SignalLightsRegisters".to_string()),
                        read_area: Some(RegisterArea::DiscreteInput),
                        write_area: None,
                        read_fields: vec![
                            ContractTypedItem {
                                name: "ch1".to_string(),
                                summary: Some("第一路".to_string()),
                                value_type: ValueType::Boolean,
                            },
                            ContractTypedItem {
                                name: "ch2".to_string(),
                                summary: Some("第二路".to_string()),
                                value_type: ValueType::Boolean,
                            },
                        ],
                        parameters: vec![],
                    },
                    ContractMethod {
                        summary: Some("读取输入寄存器里的布尔配置".to_string()),
                        method_name: "getInputRegisterFlags".to_string(),
                        read_return_kind: Some(ReadReturnKind::Dto),
                        read_return_type_name: Some("InputRegisterFlags".to_string()),
                        read_area: Some(RegisterArea::HoldingRegister),
                        write_area: None,
                        read_fields: vec![
                            ContractTypedItem {
                                name: "enabled".to_string(),
                                summary: Some("启用状态".to_string()),
                                value_type: ValueType::Boolean,
                            },
                            ContractTypedItem {
                                name: "ready".to_string(),
                                summary: Some("就绪状态".to_string()),
                                value_type: ValueType::Boolean,
                            },
                        ],
                        parameters: vec![],
                    },
                ],
            },
            ContractService {
                api_kind: ApiKind::Write,
                interface_name: "DeviceWriteApi".to_string(),
                interface_summary: Some("写操作".to_string()),
                methods: vec![
                    ContractMethod {
                        summary: Some("设置故障灯".to_string()),
                        method_name: "writeIndicatorLights".to_string(),
                        read_return_kind: None,
                        read_return_type_name: None,
                        read_area: None,
                        write_area: Some(RegisterArea::Coil),
                        read_fields: vec![],
                        parameters: vec![
                            ContractTypedItem {
                                name: "faultLightOn".to_string(),
                                summary: None,
                                value_type: ValueType::Boolean,
                            },
                            ContractTypedItem {
                                name: "runLightOn".to_string(),
                                summary: None,
                                value_type: ValueType::Boolean,
                            },
                        ],
                    },
                    ContractMethod {
                        summary: Some("写入寄存器配置".to_string()),
                        method_name: "writeRuntimeProfile".to_string(),
                        read_return_kind: None,
                        read_return_type_name: None,
                        read_area: None,
                        write_area: Some(RegisterArea::HoldingRegister),
                        read_fields: vec![],
                        parameters: vec![
                            ContractTypedItem {
                                name: "sampleIntervalMs".to_string(),
                                summary: Some("采样间隔".to_string()),
                                value_type: ValueType::Int,
                            },
                            ContractTypedItem {
                                name: "profileName".to_string(),
                                summary: Some("档位名称".to_string()),
                                value_type: ValueType::String,
                            },
                        ],
                    },
                ],
            },
        ],
    }
}

fn create_temp_root(prefix: &str) -> PathBuf {
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let process_id = std::process::id();
    let directory = std::env::temp_dir().join(format!("{prefix}-{process_id}-{timestamp}"));
    fs::create_dir_all(&directory).expect("创建临时目录失败");
    directory
}

fn write_generated_bundle(bundle: &GeneratedBundle, root: &Path) -> Result<(), Box<dyn Error>> {
    for artifact in &bundle.artifacts {
        write_artifact(root, artifact)?;
    }
    Ok(())
}

fn write_artifact(root: &Path, artifact: &GeneratedArtifact) -> Result<(), Box<dyn Error>> {
    let path = root.join(&artifact.relative_path);
    let parent = path.parent().ok_or("生成产物缺少父目录")?;
    fs::create_dir_all(parent)?;
    fs::write(path, &artifact.content)?;
    Ok(())
}

fn compile_generated_c_sources(root: &Path) -> Result<(), Box<dyn Error>> {
    let include_root = root.join("Core/Inc/generated/modbus");
    let object_root = root.join("c-objects");
    fs::create_dir_all(&object_root)?;
    let sources = [
        root.join("Core/Src/generated/modbus/okm_modbus_dispatch.c"),
        root.join("Core/Src/generated/modbus/rtu/okm_modbus_rtu.c"),
        root.join("Core/Src/generated/modbus/tcp/okm_modbus_tcp.c"),
    ];
    for source in sources {
        let object_path = object_root.join(
            source
                .file_name()
                .ok_or("C 源文件名不存在")?
                .to_string_lossy()
                .replace(".c", ".o"),
        );
        run_command(
            Command::new("cc")
                .arg("-std=c99")
                .arg("-Wall")
                .arg("-Wextra")
                .arg("-I")
                .arg(&include_root)
                .arg("-c")
                .arg(&source)
                .arg("-o")
                .arg(object_path),
            "编译生成的 C 源文件失败",
        )?;
    }
    Ok(())
}

fn compile_generated_rust_artifact(
    root: &Path,
    bundle: &GeneratedBundle,
    segment: &str,
) -> Result<(), Box<dyn Error>> {
    let artifact = bundle
        .artifacts
        .iter()
        .find(|item| item.file_name == format!("okm_modbus_{segment}_tokio_client"))
        .ok_or("缺少目标 Rust 产物")?;
    let crate_root = root.join(format!("rust-check-{segment}"));
    let source_root = crate_root.join("src");
    fs::create_dir_all(&source_root)?;
    fs::write(
        crate_root.join("Cargo.toml"),
        r#"[package]
name = "generated-rust-check"
version = "0.1.0"
edition = "2021"

[lib]
path = "src/lib.rs"

[dependencies]
serialport = "4.8"
tokio-modbus = { version = "0.17", default-features = false, features = ["rtu-sync", "tcp-sync"] }
"#,
    )?;
    fs::write(source_root.join("lib.rs"), &artifact.content)?;
    let target_dir = root.join("cargo-target");
    run_command(
        Command::new("cargo")
            .arg("check")
            .arg("--quiet")
            .env("CARGO_TARGET_DIR", &target_dir)
            .current_dir(&crate_root),
        "编译生成的 Rust 产物失败",
    )?;
    Ok(())
}

fn run_command(command: &mut Command, error_context: &str) -> Result<(), Box<dyn Error>> {
    let output = command.output()?;
    if output.status.success() {
        return Ok(());
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    Err(format!(
        "{error_context}\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
        output.status, stdout, stderr
    )
    .into())
}