alef 0.24.9

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::backends::gleam::naming::gleam_app_name;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::template_versions::hex;
use crate::scaffold::scaffold_meta;
use std::path::PathBuf;

pub(crate) fn scaffold_gleam(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    let meta = scaffold_meta(config);
    let version = &api.version;
    let gleam_app = gleam_app_name(config);
    let license = meta.license.as_deref().ok_or_else(|| {
        anyhow::anyhow!(
            "Gleam scaffold requires package metadata license; set package_metadata.license or scaffold.license"
        )
    })?;

    let gleam_toml = format!(
        r#"name = "{app_name}"
version = "{version}"
description = "{description}"
licences = ["{license}"]
target = "erlang"

[dependencies]
gleam_stdlib = "{stdlib}"

[dev-dependencies]
gleeunit = "{gleeunit}"
"#,
        app_name = gleam_app,
        version = version,
        description = meta.description,
        license = license,
        stdlib = hex::GLEAM_STDLIB_VERSION_RANGE,
        gleeunit = hex::GLEEUNIT_VERSION_RANGE,
    );

    // manifest.toml is normally generated by `gleam build` / `gleam update` as a
    // lockfile, but `gleam check` and `gleam build` both fail-fast if the file
    // exists with the wrong shape. An empty (comment-only) manifest is parsed
    // by gleam's TOML reader and rejected with `missing field 'requirements'`.
    // Emit a minimal-but-valid manifest with empty `packages` and an empty
    // `[requirements]` table — gleam will repopulate it on the first build.
    let manifest_toml =
        "# This file is generated by gleam build\n# Do not manually edit\n\npackages = []\n\n[requirements]\n";

    let gitignore = "build/\n";

    // Smoke test that exercises the generated module loads. `gleam test` requires a
    // `test/<app_name>_test.gleam` entry point; without it the test runner errors out.
    // We don't import the generated module here to avoid forcing reference to specific
    // symbols that may shift across regenerations; projects add real assertions.
    let smoke_test = r#"import gleeunit
import gleeunit/should

pub fn main() {
  gleeunit.main()
}

/// Smoke test: confirms the test runner is wired up. Replace with real tests
/// that import the generated module and exercise its functions.
pub fn smoke_test() {
  should.equal(1, 1)
}
"#
    .to_string();

    let editorconfig = "[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\n\n[*.gleam]\nindent_style = space\nindent_size = 2\n";

    let readme = format!(
        r#"# {gleam_app}

{description}

## Building

Install Gleam (see [gleam.run](https://gleam.run)):

```sh
gleam build
gleam test
```

## Usage

Add to your `gleam.toml`:

```toml
[dependencies]
{gleam_app} = {dependency_ref}
```

## License

{license}
"#,
        gleam_app = gleam_app,
        description = meta.description,
        dependency_ref = gleam_dependency_ref(&meta),
        license = license,
    );

    let example_gleam = format!(
        r#"import {gleam_app}

pub fn main() {{
  // Example: load and use the generated {gleam_app} module
  // Replace with your actual API calls after code generation
  Nil
}}
"#,
        gleam_app = gleam_app,
    );

    Ok(vec![
        GeneratedFile {
            path: PathBuf::from("packages/gleam/gleam.toml"),
            content: gleam_toml,
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from("packages/gleam/manifest.toml"),
            content: manifest_toml.to_string(),
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from("packages/gleam/.gitignore"),
            content: gitignore.to_string(),
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from(format!("packages/gleam/test/{gleam_app}_test.gleam")),
            content: smoke_test,
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from("packages/gleam/.editorconfig"),
            content: editorconfig.to_string(),
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from("packages/gleam/README.md"),
            content: readme,
            generated_header: false,
        },
        GeneratedFile {
            path: PathBuf::from(format!("packages/gleam/src/{gleam_app}_example.gleam")),
            content: example_gleam,
            generated_header: false,
        },
    ])
}

fn gleam_dependency_ref(meta: &crate::scaffold::ScaffoldMeta) -> String {
    let Some(repository) = meta.configured_repository.as_deref() else {
        return "{path = \"../packages/gleam\"}".to_string();
    };
    let github_path = repository
        .strip_prefix("https://github.com/")
        .or_else(|| repository.strip_prefix("http://github.com/"))
        .map(|path| path.trim_end_matches('/').trim_end_matches(".git"));
    if let Some(path) = github_path
        && !path.is_empty()
    {
        return format!("{{github = \"{path}\"}}");
    }
    format!("{{git = \"{}\"}}", repository.trim_end_matches(".git"))
}