puerto 0.17.0

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
//! The runtime crates a generated project depends on (ADR-0017): which ones, at which version.

/// Stands for the runtime requirement in the template's root `Cargo.toml.liquid`. Replaced when
/// the template is extracted, so the version follows the CLI's own and is never hard-coded.
pub const VERSION_MARKER: &str = "PUERTO_RUNTIME_VERSION";

/// Every runtime crate a generated project may depend on.
pub const RUNTIME_CRATES: &[&str] = &[
    "puerto-core",
    "puerto-web",
    "puerto-sqlx",
    "puerto-events",
    "puerto-kafka",
];

/// What a project generated by CLI `version` asks of the runtime: its `major.minor`. The CLI and
/// the runtime crates share one version, so a patch release of either is picked up by `cargo
/// update` and a minor is a deliberate upgrade.
pub fn requirement(version: &str) -> String {
    let mut parts = version.split('.');
    match (parts.next(), parts.next()) {
        (Some(major), Some(minor)) => format!("{major}.{minor}"),
        _ => version.to_string(),
    }
}

/// The template's root manifest, with the runtime requirement for CLI `version` in place.
pub fn render_manifest(manifest: &str, version: &str) -> String {
    manifest.replace(VERSION_MARKER, &requirement(version))
}

/// One warning per runtime crate that `manifest` (a `Cargo.toml`) requires on another minor than
/// CLI `cli_version` — the generated code then expects a runtime the project does not use.
pub fn version_warnings(manifest: &str, cli_version: &str) -> Vec<String> {
    let Ok(manifest) = manifest.parse::<toml::Table>() else {
        return vec![];
    };
    let tables = [
        manifest
            .get("workspace")
            .and_then(|w| w.get("dependencies")),
        manifest.get("dependencies"),
    ];
    let cli = requirement(cli_version);
    let mut warnings = vec![];
    for deps in tables.into_iter().flatten() {
        for name in RUNTIME_CRATES {
            let declared = match deps.get(*name) {
                Some(toml::Value::String(v)) => v.as_str(),
                Some(toml::Value::Table(t)) => match t.get("version") {
                    Some(toml::Value::String(v)) => v.as_str(),
                    _ => continue,
                },
                _ => continue,
            };
            let declared = requirement(declared.trim_start_matches(['^', '=', '~']));
            if declared != cli {
                warnings.push(format!(
                    "{name} {declared} but puerto {cli} — see the CHANGELOG migration guide"
                ));
            }
        }
    }
    warnings
}