forklaunch 1.35.0

Launch faster with forklaunch
//! Host-compilable contract ownership for ordinary native projects.
use super::*;

/// Resolve the declared contract package, retaining existing nested layouts.
pub(super) fn directory(dir: &Path) -> Option<PathBuf> {
    let cargo: Value = toml::from_str(&read_to_string(dir.join("Cargo.toml")).ok()?).ok()?;
    let path = cargo
        .get("build-dependencies")?
        .get("contracts")?
        .get("path")?
        .as_str()?;
    Some(dir.join(path))
}
pub(super) fn is_native_service(dir: &Path) -> bool {
    directory(dir).is_some_and(|path| path.join("Cargo.toml").is_file())
        && dir.join("api/routes").is_dir()
        // Built-in modules share this host/SDK boundary, but their extension
        // routers bind to the module adapter rather than an application AppState.
        && !dir.join("api/contracts/registry.rs").is_file()
}

pub(super) fn has_local_sdk(dir: &Path) -> bool {
    dir.join("sdk/rust/.forklaunch-sdk-local").is_file()
}
/// Match the outputs owned by core::build::service_sdks and its provenance receipt.
/// AsyncAPI lives beside them so every per-service artifact shares one folder.
pub(super) fn set_artifact_paths(metadata: &mut Map<String, Value>, name: &str) {
    for (key, file) in [
        ("contractArtifactPath", "contract.json"),
        ("openapiPath", "openapi.json"),
        ("asyncapiPath", "asyncapi.json"),
    ] {
        metadata.insert(
            key.into(),
            Value::String(
                if metadata.get("sdkLayout").and_then(Value::as_str) == Some("project-local-v1") {
                    format!(
                        "{}/sdk/typescript/{file}",
                        metadata
                            .get("cratePath")
                            .and_then(Value::as_str)
                            .unwrap_or(&format!("src/{name}"))
                    )
                } else {
                    format!("src/client-sdk/typescript/{name}/{file}")
                },
            ),
        );
    }
}
pub(super) fn source(dir: &Path, relative: &str) -> PathBuf {
    if directory(dir).is_some_and(|host| host == new_host_directory(dir)) {
        return dir.join(relative);
    }
    let host = directory(dir).filter(|path| path.join("lib.rs").is_file());
    match (
        host,
        relative.strip_prefix("api/contracts/"),
        relative.strip_prefix("domain/schemas/"),
    ) {
        (Some(host), Some(path), _) => host.join("endpoints").join(path),
        (Some(host), _, Some(path)) => host.join("schemas").join(path),
        _ => dir.join(relative),
    }
}
fn split_sibling(dir: &Path, name: &str, files: Vec<RenderedTemplate>) -> Vec<RenderedTemplate> {
    if dir.join("domain/Cargo.toml").is_file() {
        return files;
    }
    let host = dir.with_file_name(format!("{name}-contracts"));
    let mut output = Vec::new();
    for mut file in files {
        let relative = file
            .path
            .strip_prefix(dir)
            .unwrap()
            .to_string_lossy()
            .replace('\\', "/");
        match relative.as_str() {
            "Cargo.toml" => {
                file.content = file.content.replace(
                    "path = \"domain\"",
                    &format!("path = \"../{name}-contracts\""),
                )
            }
            "domain/Cargo.toml" => {
                file.path = host.join("Cargo.toml");
                file.content = file
                    .content
                    .replace("path = \"../sdk.rs\"", "path = \"lib.rs\"")
                    .replace("../../../.forklaunch/", "../../.forklaunch/");
            }
            "domain/Makefile" => file.path = host.join("Makefile"),
            "sdk.rs" => {
                file.content = "//! Public contract exports.\npub use contracts::*;\n".into();
                output.push(RenderedTemplate { path: host.join("lib.rs"), content: "//! Wire declarations shared by runtime and host compiler.\npub mod schemas;\nmod endpoints;\npub use endpoints::*;\n".into(), context: None });
            }
            "build.rs" => {
                file.content = file.content.replacen("let evidence =", &format!("println!(\"cargo:rerun-if-changed=../{name}-contracts\");\n    let evidence ="), 1);
            }
            _ if relative.starts_with("api/contracts/") => {
                let name = relative.strip_prefix("api/contracts/").unwrap();
                let export = if name == "mod.rs" {
                    "pub use contracts::*;\n".into()
                } else {
                    format!("pub use contracts::{}::*;\n", name.trim_end_matches(".rs"))
                };
                output.push(RenderedTemplate {
                    path: file.path.clone(),
                    content: export,
                    context: None,
                });
                file.path = host.join("endpoints").join(name);
            }
            _ if relative.starts_with("domain/schemas/") => {
                let name = relative.strip_prefix("domain/schemas/").unwrap();
                output.push(RenderedTemplate {
                    path: file.path.clone(),
                    content: "pub use contracts::schemas::*;\n".into(),
                    context: None,
                });
                file.path = host.join("schemas").join(name);
            }
            _ => {}
        }
        output.push(file);
    }
    output
}

/// Generated private compilation boundary; application declarations remain local.
pub(super) fn new_host_directory(dir: &Path) -> PathBuf {
    dir.join(".forklaunch/contracts")
}

pub(super) fn split(dir: &Path, name: &str, files: Vec<RenderedTemplate>) -> Vec<RenderedTemplate> {
    if dir.join("domain/Cargo.toml").is_file()
        || dir
            .with_file_name(format!("{name}-contracts"))
            .join("Cargo.toml")
            .is_file()
    {
        return split_sibling(dir, name, files);
    }
    let host = new_host_directory(dir);
    let mut output = Vec::new();
    let mut inline = Vec::new();
    for mut file in files {
        let relative = file
            .path
            .strip_prefix(dir)
            .unwrap()
            .to_string_lossy()
            .replace('\\', "/");
        match relative.as_str() {
            "Cargo.toml" => file.content = file.content.replace("path = \"domain\"", "path = \".forklaunch/contracts\""),
            "domain/Cargo.toml" => {
                file.path=host.join("Cargo.toml");
                file.content=file.content.replace("[package]", "[package]\npublish = false").replace("path = \"../sdk.rs\"", "path = \"lib.rs\"");
            }
            "domain/Makefile" | "sdk.rs" | "api/contracts/mod.rs" => continue,
            _ if relative.starts_with("api/contracts/") => {
                let module=Path::new(&relative).file_stem().unwrap().to_string_lossy().into_owned();
                inline.push((module, inline_contract(&file.content)));
                continue;
            }
            "build.rs" => file.content=file.content.replace("api/contracts", "api/controllers").replace("    println!(\"cargo:rerun-if-changed=sdk.rs\");\n", "").replacen("let evidence =", "println!(\"cargo:rerun-if-changed=.forklaunch/contracts\");\n    let evidence =",1),
            _ if relative.starts_with("api/routes/") => {
                file.content=file.content.replace("contracts::health::", "crate::api::controllers::health::contract::");
            }
            _ => {}
        }
        output.push(file);
    }
    for (module, declaration) in &inline {
        let file = output
            .iter_mut()
            .find(|file| file.path == dir.join(format!("api/controllers/{module}.rs")))
            .expect("controller for generated contract");
        file.content.push_str(&format!("\n{declaration}\n"));
    }
    output.push(RenderedTemplate {
        path: host.join("lib.rs"),
        content: host_source(
            name,
            &inline.into_iter().map(|(name, _)| name).collect::<Vec<_>>(),
        ),
        context: None,
    });
    output.extend(sdk_consumer::local_templates(dir, name, &host));
    output
}

/// Preserve ordinary Rust authoring while exporting only the explicit API module.
pub(super) fn inline_contract(source: &str) -> String {
    source
        .split("pub use operations")
        .next()
        .unwrap_or(source)
        .replace("mod operations", "pub mod contract")
        .replace("crate::schemas::", "crate::domain::schemas::")
}
pub(super) fn host_source(name: &str, modules: &[String]) -> String {
    let exports=modules.iter().map(|module|format!("forklaunch_core::contract::controller_contract!({module}, \"../../api/controllers/{module}.rs\");\n")).collect::<String>();
    let routes = modules
        .iter()
        .map(|module| {
            format!("routes.extend({module}::contract::registry().routes().iter().cloned());\n")
        })
        .collect::<String>();
    let bindings = modules
        .iter()
        .map(|module| format!("declarations.extend({module}::contract::binding_manifest()?);\n"))
        .collect::<String>();
    format!(
        "//! Generated private compiler host; edit controller contracts and domain schemas.\n#[path=\"../../domain/schemas/mod.rs\"]\npub mod schemas;\npub mod domain {{pub use crate::schemas;}}\n{exports}\npub fn routes()->Vec<forklaunch_core::contract::RouteContract>{{let mut routes=Vec::new();{routes}routes}}\npub fn registry()->forklaunch_core::contract::ContractRegistry{{let mut registry=forklaunch_core::contract::ContractRegistry::for_service(forklaunch_core::contract::ServiceContract::new(\"{name}\",env!(\"CARGO_PKG_VERSION\")));for route in routes(){{registry.register(route);}}registry}}\npub fn binding_manifest()->Result<Vec<forklaunch_core::contract::assembly::BindingDeclaration>,String>{{let mut declarations=Vec::new();{bindings}Ok(declarations)}}\n"
    )
}