use crate::backends::gleam::naming::{gleam_app_name, gleam_nif_module};
use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use heck::ToUpperCamelCase;
use minijinja::{Environment, Value};
fn to_pascal_case(s: &str) -> String {
s.to_upper_camel_case()
}
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
mod template_env;
pub fn generate_readmes(
api: &ApiSurface,
config: &ResolvedCrateConfig,
languages: &[Language],
) -> anyhow::Result<Vec<GeneratedFile>> {
let mut files = vec![];
for &lang in languages {
if let Some(file) = generate_readme(api, config, lang)? {
files.push(file);
}
}
Ok(files)
}
fn generate_readme(
api: &ApiSurface,
config: &ResolvedCrateConfig,
lang: Language,
) -> anyhow::Result<Option<GeneratedFile>> {
if matches!(lang, Language::Rust) && !rust_readme_explicitly_configured(config) {
return Ok(None);
}
if matches!(lang, Language::C | Language::Jni) {
return Ok(None);
}
if let Some(readme_cfg) = &config.readme {
if let Some(template_dir) = &readme_cfg.template_dir {
let workspace_root = config.workspace_root.clone().unwrap_or_else(|| PathBuf::from("."));
let abs_template_dir = workspace_root.join(template_dir);
if abs_template_dir.exists() {
if let Some(file) =
try_template_readme(api, config, lang, readme_cfg, &workspace_root, &abs_template_dir)?
{
return Ok(Some(file));
}
}
}
}
Ok(Some(generate_readme_hardcoded(api, config, lang)?))
}
fn rust_readme_explicitly_configured(config: &ResolvedCrateConfig) -> bool {
let Some(readme_cfg) = &config.readme else {
return false;
};
let Some(rust_cfg) = readme_cfg.languages.get("rust") else {
return false;
};
rust_cfg
.get("output_path")
.or_else(|| rust_cfg.get("output"))
.and_then(|v| v.as_str())
.is_some()
}
fn try_template_readme(
api: &ApiSurface,
config: &ResolvedCrateConfig,
lang: Language,
readme_cfg: &crate::core::config::ReadmeConfig,
workspace_root: &Path,
abs_template_dir: &Path,
) -> anyhow::Result<Option<GeneratedFile>> {
let lang_code = lang_code(lang);
let lang_json: Option<serde_json::Value> = if !readme_cfg.languages.is_empty() {
readme_cfg.languages.get(lang_code).cloned()
} else if let Some(config_path) = &readme_cfg.config {
let abs_config = workspace_root.join(config_path);
if abs_config.exists() {
let content = fs::read_to_string(&abs_config)
.map_err(|e| anyhow::anyhow!("Failed to read readme config {:?}: {}", abs_config, e))?;
let yaml: serde_yaml::Value = serde_yaml::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse readme config YAML: {}", e))?;
let as_json = serde_json::to_value(&yaml)
.map_err(|e| anyhow::anyhow!("Failed to convert readme YAML to JSON: {}", e))?;
as_json.get("languages").and_then(|l| l.get(lang_code)).cloned()
} else {
None
}
} else {
None
};
let Some(lang_json) = lang_json else {
return Ok(None);
};
let discord_url = readme_cfg.discord_url.as_deref().unwrap_or("").to_string();
let banner_url = readme_cfg.banner_url.as_deref().unwrap_or("").to_string();
let template_name = lang_json
.get("template")
.and_then(|v| v.as_str())
.unwrap_or("language_package.md")
.to_string();
let template_file = abs_template_dir.join(&template_name);
if !template_file.exists() {
return Ok(None);
}
let abs_template_dir_owned = abs_template_dir.to_path_buf();
let mut env = Environment::new();
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.set_keep_trailing_newline(true);
env.set_loader(move |name: &str| {
let path = abs_template_dir_owned.join(name);
match fs::read_to_string(&path) {
Ok(content) => Ok(Some(content)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!("Failed to read template {name}: {e}"),
)),
}
});
let snippets_dir = readme_cfg.snippets_dir.as_ref().map(|s| workspace_root.join(s));
let snippets_dir_clone = snippets_dir.clone();
env.add_filter("include_snippet", move |path: String, language: String| -> String {
match &snippets_dir_clone {
Some(dir) => include_snippet(dir, &language, &path),
None => format!("<!-- snippet not found: {path} -->"),
}
});
env.add_filter(
"render_performance_table",
|benchmarks: Value, name: String| -> String { render_performance_table(&benchmarks, &name) },
);
let workspace_root_clone = workspace_root.to_path_buf();
env.add_function("has_migration", move |_lang: String, _version: String| -> bool {
let path = workspace_root_clone
.join("docs")
.join("migrations")
.join(&_lang)
.join(format!("{_version}.md"));
path.exists()
});
let name = &config.name;
let description = config
.scaffold
.as_ref()
.and_then(|s| s.description.clone())
.map(|s| s.trim_end().to_string())
.unwrap_or_else(|| format!("Bindings for {name}"));
let repository = config.github_repo();
let license = config
.scaffold
.as_ref()
.and_then(|s| s.license.clone())
.unwrap_or_else(|| "MIT".to_string());
let mut ctx: HashMap<&str, Value> = HashMap::new();
ctx.insert("version", Value::from(api.version.clone()));
ctx.insert("name", Value::from(name.clone()));
ctx.insert("description", Value::from(description));
ctx.insert("license", Value::from(license));
ctx.insert("repository", Value::from(repository));
ctx.insert("discord_url", Value::from(discord_url));
ctx.insert("banner_url", Value::from(banner_url));
ctx.insert("language", Value::from(lang_code.to_string()));
if let serde_json::Value::Object(map) = &lang_json {
for (key, val) in map {
let rendered_val = if let serde_json::Value::String(s) = val {
if s.contains("{{") {
let rendered = env.render_str(s, &ctx).unwrap_or_else(|_| s.clone());
Value::from(rendered)
} else {
json_to_minijinja_value(val)
}
} else {
json_to_minijinja_value(val)
};
ctx.insert(
Box::leak(key.clone().into_boxed_str()),
rendered_val,
);
}
}
ctx.entry("snippets")
.or_insert_with(|| json_to_minijinja_value(&serde_json::Value::Object(Default::default())));
let tmpl = env
.get_template(&template_name)
.map_err(|e| anyhow::anyhow!("Failed to load template '{}': {}", template_name, e))?;
let mut content = tmpl
.render(ctx)
.map_err(|e| anyhow::anyhow!("Failed to render template '{}': {}", template_name, e))?;
if !content.ends_with('\n') {
content.push('\n');
}
let path = readme_output_path(config, lang, readme_cfg, &lang_json);
Ok(Some(GeneratedFile {
path,
content,
generated_header: false,
}))
}
fn readme_output_path(
config: &ResolvedCrateConfig,
lang: Language,
readme_cfg: &crate::core::config::ReadmeConfig,
lang_json: &serde_json::Value,
) -> PathBuf {
if let Some(output) = lang_json
.get("output_path")
.or_else(|| lang_json.get("output"))
.and_then(|v| v.as_str())
{
return PathBuf::from(output);
}
if let Some(pattern) = &readme_cfg.output_pattern {
let dir = lang_dir_name(lang);
return PathBuf::from(pattern.replace("{language}", dir));
}
default_readme_path(config, lang)
}
fn default_readme_path(config: &ResolvedCrateConfig, lang: Language) -> PathBuf {
let name = &config.name;
match lang {
Language::Ffi => PathBuf::from(format!("crates/{name}-ffi/README.md")),
Language::Wasm => PathBuf::from(format!("crates/{name}-wasm/README.md")),
Language::Node => PathBuf::from(format!("crates/{name}-node/README.md")),
Language::Rust => PathBuf::from(format!("crates/{name}/README.md")),
_ => PathBuf::from(format!("packages/{}/README.md", lang_dir_name(lang))),
}
}
fn lang_dir_name(lang: Language) -> &'static str {
match lang {
Language::Python => "python",
Language::Node => "node",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Elixir => "elixir",
Language::Go => "go",
Language::Java => "java",
Language::Csharp => "csharp",
Language::Ffi => "ffi",
Language::Wasm => "wasm",
Language::R => "r",
Language::Rust => "rust",
Language::Kotlin => "kotlin",
Language::KotlinAndroid => "kotlin-android",
Language::Swift => "swift",
Language::Dart => "dart",
Language::Gleam => "gleam",
Language::Zig => "zig",
Language::C | Language::Jni => "c",
}
}
fn lang_code(lang: Language) -> &'static str {
match lang {
Language::Python => "python",
Language::Node => "typescript",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Elixir => "elixir",
Language::Go => "go",
Language::Java => "java",
Language::Csharp => "csharp",
Language::Ffi => "ffi",
Language::Wasm => "wasm",
Language::R => "r",
Language::Rust => "rust",
Language::Kotlin => "kotlin",
Language::KotlinAndroid => "kotlin_android",
Language::Swift => "swift",
Language::Dart => "dart",
Language::Gleam => "gleam",
Language::Zig => "zig",
Language::C | Language::Jni => "c",
}
}
fn include_snippet(snippets_dir: &Path, lang_code: &str, path: &str) -> String {
let file = snippets_dir.join(lang_code).join(path);
if !file.exists() {
return format!("<!-- snippet not found: {path} -->");
}
let content = fs::read_to_string(&file).unwrap_or_default();
if path.ends_with(".md") {
extract_code_block(&content)
} else {
let ext = Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("");
format!("```{ext}\n{}\n```", content.trim())
}
}
fn extract_code_block(md: &str) -> String {
let mut in_block = false;
let mut block_lines: Vec<&str> = vec![];
let mut fence_marker = "";
for line in md.lines() {
if !in_block {
if line.starts_with("```") || line.starts_with("~~~") {
in_block = true;
fence_marker = if line.starts_with("```") { "```" } else { "~~~" };
block_lines.push(line);
}
} else {
block_lines.push(line);
if line.trim() == fence_marker {
break;
}
}
}
if block_lines.is_empty() {
md.to_string()
} else {
block_lines.join("\n")
}
}
fn render_performance_table(perf: &Value, _name: &str) -> String {
use minijinja::value::ValueKind;
let platform = perf
.get_attr("platform")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let function = perf
.get_attr("function")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let note = perf
.get_attr("note")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let benchmarks = match perf.get_attr("benchmarks") {
Ok(v) if v.kind() == ValueKind::Seq => v,
_ => return String::new(),
};
let Ok(iter) = benchmarks.try_iter() else {
return String::new();
};
let mut out = String::new();
if !platform.is_empty() {
out.push_str(&template_env::render(
"performance_context.jinja",
minijinja::context! { platform => platform, function => function, note => note },
));
out.push('\n');
}
let items: Vec<Value> = iter.collect();
let has_throughput = items
.iter()
.any(|item| item.get_attr("throughput").ok().is_some_and(|v| !v.is_undefined()));
if has_throughput {
out.push_str("| Document | Size | Latency | Throughput |\n");
out.push_str("|----------|------|---------|------------|\n");
for item in &items {
let name = item
.get_attr("name")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let size = item
.get_attr("size")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let latency = item
.get_attr("latency")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let throughput = item
.get_attr("throughput")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
out.push_str(&template_env::render(
"performance_throughput_row.jinja",
minijinja::context! { name => name, size => size, latency => latency, throughput => throughput },
));
}
} else {
out.push_str("| Document | Size | Ops/sec |\n");
out.push_str("|----------|------|---------|\n");
for item in &items {
let name = item
.get_attr("name")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let size = item
.get_attr("size")
.ok()
.and_then(|v: Value| v.as_str().map(str::to_string))
.unwrap_or_default();
let ops = item
.get_attr("ops_sec")
.ok()
.map(|v: Value| format!("{v}"))
.unwrap_or_default();
out.push_str(&template_env::render(
"performance_ops_row.jinja",
minijinja::context! { name => name, size => size, ops => ops },
));
}
}
out
}
fn json_to_minijinja_value(json: &serde_json::Value) -> Value {
Value::from_serialize(json)
}
fn generate_readme_hardcoded(
api: &ApiSurface,
config: &ResolvedCrateConfig,
lang: Language,
) -> anyhow::Result<GeneratedFile> {
let name = &config.name;
let description = config
.scaffold
.as_ref()
.and_then(|s| s.description.clone())
.unwrap_or_else(|| format!("Bindings for {}", name));
let repository = config.github_repo();
let example_pointer = format!("See {repository} for usage examples.");
let (lang_display, install_instructions, example_code, dir_name) = match lang {
Language::Python => {
let module = config.python_module_name().trim_start_matches('_').to_string();
let example_body = api
.functions
.first()
.map(|f| {
format!(
"# result = {module}.{name}(...)\n# See the main repository's docs for full usage.",
name = f.name
)
})
.unwrap_or_else(|| format!("# {example_pointer}"));
(
"Python",
format!("```bash\npip install {name}\n```"),
format!("```python\nimport {module}\n\n{example_body}\n```"),
"python",
)
}
Language::Node => {
let pkg = config.node_package_name();
let example_body = api
.functions
.first()
.map(|f| {
format!(
"// const result = await {fname}(...);\n// See the main repository's docs for full usage.",
fname = to_camel(&f.name)
)
})
.unwrap_or_else(|| format!("// {example_pointer}"));
(
"Node.js",
format!("```bash\nnpm install {pkg}\n```"),
format!("```typescript\nimport {{ /* ... */ }} from '{pkg}';\n\n{example_body}\n```"),
"node",
)
}
Language::Ruby => {
let gem = config.ruby_gem_name();
let example_body = format!("# {example_pointer}");
(
"Ruby",
format!("```bash\ngem install {gem}\n```"),
format!("```ruby\nrequire '{gem}'\n\n{example_body}\n```"),
"ruby",
)
}
Language::Php => {
let ext = config.php_extension_name();
let example_body = format!("// {example_pointer}");
let vendor = config
.try_github_repo()
.ok()
.as_deref()
.and_then(crate::core::config::derive_repo_org)
.unwrap_or_else(|| name.clone());
(
"PHP",
format!("```bash\ncomposer require {vendor}/{name}\n```"),
format!("```php\n<?php\n\nuse {ext};\n\n{example_body}\n```"),
"php",
)
}
Language::Elixir => {
let app = config.elixir_app_name();
let module = capitalize_first(&app);
let example_body = format!("# {example_pointer}");
(
"Elixir",
format!(
"Add `:{app}` to your `mix.exs` dependencies:\n\n```elixir\ndefp deps do\n [\n {{:{app}, \"~> {version}\"}}\n ]\nend\n```",
version = api.version,
),
format!("```elixir\n{module}.hello()\n\n{example_body}\n```"),
"elixir",
)
}
Language::Go => {
let module = config.go_module();
let example_body = format!("\t// {example_pointer}");
(
"Go",
format!("```bash\ngo get {module}\n```"),
format!("```go\npackage main\n\nimport \"{module}\"\n\nfunc main() {{\n{example_body}\n}}\n```"),
"go",
)
}
Language::Java => {
let package = config.java_package();
let example_body = format!("// {example_pointer}");
(
"Java",
format!(
"Add to your `pom.xml`:\n\n```xml\n<dependency>\n <groupId>{package}</groupId>\n <artifactId>{name}</artifactId>\n <version>{version}</version>\n</dependency>\n```",
version = api.version,
),
format!("```java\nimport {package}.*;\n\n{example_body}\n```"),
"java",
)
}
Language::Csharp => {
let ns = config.csharp_namespace();
let example_body = format!("// {example_pointer}");
(
"C#",
format!("```bash\ndotnet add package {ns}\n```"),
format!("```csharp\nusing {ns};\n\n{example_body}\n```"),
"csharp",
)
}
Language::Ffi => {
let header = config.ffi_header_name();
let example_body = format!(" // {example_pointer}");
(
"FFI (C/C++)",
format!(
"Link against `lib{name}_ffi` and include `{header}`.\n\nSee the build instructions in the main repository.",
),
format!("```c\n#include \"{header}\"\n\nint main(void) {{\n{example_body}\n return 0;\n}}\n```"),
"ffi",
)
}
Language::Wasm => {
let example_body = format!("// {example_pointer}");
(
"WebAssembly",
format!("```bash\nnpm install {name}-wasm\n```"),
format!("```javascript\nimport init from '{name}-wasm';\n\nawait init();\n{example_body}\n```"),
"wasm",
)
}
Language::R => {
let pkg = config.r_package_name();
let example_body = format!("# {example_pointer}");
(
"R",
format!("```r\ninstall.packages('{pkg}')\n```"),
format!("```r\nlibrary({pkg})\n\n{example_body}\n```"),
"r",
)
}
Language::Rust => {
let import = config.core_import_name();
let example_body = format!("// {example_pointer}");
(
"Rust",
format!("```bash\ncargo add {name}\n```"),
format!("```rust\nuse {import};\n\n{example_body}\n```"),
"rust",
)
}
Language::Kotlin => {
let module = config.name.replace('-', "_");
(
"Kotlin",
format!(
"Add the generated package to your `build.gradle.kts`:\n\n```kotlin\ndependencies {{\n implementation(\"{}:{}:VERSION\")\n}}\n```",
config.kotlin_package(),
module
),
format!(
"```kotlin\nimport {}.{}\n\n// Call generated APIs through the {} object.\n```",
config.kotlin_package(),
to_pascal_case(&config.name),
to_pascal_case(&config.name)
),
"kotlin",
)
}
Language::KotlinAndroid => {
let module = config.name.replace('-', "_");
(
"Kotlin/Android",
format!(
"Add the generated AAR to your Android module's `build.gradle.kts`:\n\n```kotlin\ndependencies {{\n implementation(\"{}:{}-android:VERSION\")\n}}\n```",
config.kotlin_package(),
module
),
format!(
"```kotlin\nimport {}.{}\n\n// The bundled native library is loaded via System.loadLibrary().\n```",
config.kotlin_package(),
to_pascal_case(&config.name)
),
"kotlin-android",
)
}
Language::Swift => (
"Swift",
format!(
"Add to `Package.swift`:\n\n```swift\n.package(url: \"<repo-url>\", from: \"{}\")\n```",
config.name
),
"```swift\n// Phase 2: Swift bindings via swift-bridge. Skeleton only.\n```".to_string(),
"swift",
),
Language::Dart => (
"Dart",
format!(
"Add to `pubspec.yaml`:\n\n```yaml\ndependencies:\n {}:\n git: <repo-url>\n```",
config.name.replace('-', "_")
),
"```dart\n// Phase 2: Dart bindings via flutter_rust_bridge. Skeleton only.\n```".to_string(),
"dart",
),
Language::Gleam => {
let app = gleam_app_name(config);
(
"Gleam",
format!("```sh\ngleam add {app}\n```"),
format!(
"```gleam\nimport {app}\n\n// Call functions exported by the generated module.\n// The NIF is loaded via `@external(erlang, \"{}\", ...)`.\n```",
gleam_nif_module(config)
),
"gleam",
)
}
Language::C | Language::Jni | Language::Zig => {
let module = config.zig_module_name();
(
"Zig",
format!(
"Add to `build.zig.zon`:\n\n```zig\n.dependencies = .{{\n .{module} = .{{ .url = \"<tarball-url>\" }},\n}};\n```"
),
format!(
"```zig\nconst {module} = @import(\"{module}\");\n\n// Call generated wrapper functions; strings allocated by the FFI must\n// be released with `{module}._free_string`.\n```"
),
"zig",
)
}
};
let content = format!(
r#"# {name} - {lang_display} Bindings
{description}
## Installation
{install}
## Quick Start
{example}
## Documentation
For full documentation, see the [{name} repository]({repository}).
## License
See the [LICENSE]({repository}/blob/main/LICENSE) file in the root repository.
"#,
name = name,
lang_display = lang_display,
description = description,
install = install_instructions,
example = example_code,
repository = repository,
);
let path = match lang {
Language::Ffi => PathBuf::from(format!("crates/{}-ffi/README.md", name)),
Language::Wasm => PathBuf::from(format!("crates/{}-wasm/README.md", name)),
Language::Node => PathBuf::from(format!("crates/{}-node/README.md", name)),
Language::Rust => PathBuf::from(format!("crates/{}/README.md", name)),
_ => PathBuf::from(format!("packages/{}/README.md", dir_name)),
};
Ok(GeneratedFile {
path,
content,
generated_header: false,
})
}
fn to_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut upper_next = false;
for (i, ch) in s.chars().enumerate() {
if ch == '_' {
upper_next = true;
} else if upper_next {
result.extend(ch.to_uppercase());
upper_next = false;
} else if i == 0 {
result.extend(ch.to_lowercase());
} else {
result.push(ch);
}
}
result
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::{NewAlefConfig, ReadmeConfig, ResolvedCrateConfig};
fn test_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python", "node"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.scaffold]
description = "Test library"
license = "MIT"
repository = "https://github.com/test/my-lib"
"#,
)
.expect("valid toml");
cfg.resolve().expect("resolve ok").remove(0)
}
fn test_api() -> ApiSurface {
ApiSurface {
crate_name: "my-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
}
}
#[test]
fn test_generate_python_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/python/README.md"));
assert!(files[0].content.contains("Python"));
assert!(files[0].content.contains("pip install"));
}
#[test]
fn test_generate_node_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Node]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-node/README.md"));
assert!(files[0].content.contains("Node.js"));
}
#[test]
fn test_generate_multiple_readmes() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python, Language::Node]).unwrap();
assert_eq!(files.len(), 2);
}
#[test]
fn test_extract_code_block() {
let md = "Some text\n\n```python\nprint('hello')\n```\n\nMore text";
let result = extract_code_block(md);
assert!(result.contains("```python"));
assert!(result.contains("print('hello')"));
}
#[test]
fn test_extract_code_block_no_block() {
let md = "Just plain text";
let result = extract_code_block(md);
assert_eq!(result, "Just plain text");
}
#[test]
fn test_render_performance_table_empty() {
let v = Value::from(Vec::<Value>::new());
let result = render_performance_table(&v, "test");
assert!(result.is_empty());
}
#[test]
fn test_include_snippet_missing() {
let result = include_snippet(Path::new("/nonexistent"), "python", "foo.py");
assert!(result.contains("snippet not found"));
}
#[test]
fn test_template_version_in_install_command() {
let tmp = std::env::temp_dir().join("alef_readme_test_version");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("test.md"), "{{ install_command }}").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"java".to_string(),
serde_json::json!({
"template": "test.md",
"install_command": "<version>{{ version }}</version>",
"output_path": "packages/java/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api(); let files = generate_readmes(&api, &config, &[Language::Java]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("<version>0.1.0</version>"),
"Expected version placeholder to be rendered, got: {}",
files[0].content,
);
assert!(
!files[0].content.contains("{{ version }}"),
"Raw template placeholder should not remain in output",
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_json_to_minijinja_value_primitives() {
let json: serde_json::Value = serde_json::from_str(r#"{"key": "value", "num": 42, "flag": true}"#).unwrap();
let mj = json_to_minijinja_value(&json);
assert!(mj.get_attr("key").is_ok());
}
#[test]
fn test_generate_readmes_empty_languages() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[]).unwrap();
assert_eq!(files.len(), 0);
}
#[test]
fn test_generate_ruby_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Ruby]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/ruby/README.md"));
assert!(files[0].content.contains("Ruby"));
assert!(files[0].content.contains("gem install"));
}
#[test]
fn test_generate_php_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Php]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/php/README.md"));
assert!(files[0].content.contains("PHP"));
assert!(files[0].content.contains("composer require"));
}
#[test]
fn test_generate_elixir_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Elixir]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/elixir/README.md"));
assert!(files[0].content.contains("Elixir"));
assert!(files[0].content.contains("mix.exs"));
}
#[test]
fn test_generate_go_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Go]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/go/README.md"));
assert!(files[0].content.contains("Go"));
assert!(files[0].content.contains("go get"));
}
#[test]
fn test_generate_java_readme_hardcoded() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Java]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/java/README.md"));
assert!(files[0].content.contains("Java"));
assert!(files[0].content.contains("pom.xml"));
}
#[test]
fn test_generate_csharp_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Csharp]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/csharp/README.md"));
assert!(files[0].content.contains("C#"));
assert!(files[0].content.contains("dotnet add package"));
}
#[test]
fn test_generate_ffi_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-ffi/README.md"));
assert!(files[0].content.contains("FFI"));
}
#[test]
fn test_generate_wasm_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Wasm]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-wasm/README.md"));
assert!(files[0].content.contains("WebAssembly"));
}
#[test]
fn test_generate_r_readme() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::R]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/r/README.md"));
assert!(files[0].content.contains("install.packages"));
}
#[test]
fn test_generate_rust_readme_skipped_by_default() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
assert!(
files.is_empty(),
"Rust README should be skipped by default, got: {:?}",
files.iter().map(|f| &f.path).collect::<Vec<_>>()
);
}
#[test]
fn test_generate_rust_readme_emitted_when_explicitly_configured() {
let mut config = test_config();
let mut readme_cfg = crate::core::config::ReadmeConfig {
template_dir: None,
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: std::collections::HashMap::new(),
};
readme_cfg.languages.insert(
"rust".to_string(),
serde_json::json!({ "output_path": "crates/my-lib/README.md" }),
);
config.readme = Some(readme_cfg);
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("Rust"));
assert!(files[0].content.contains("cargo add"));
}
#[test]
fn test_generate_readme_without_scaffold_uses_placeholder() {
let mut config = test_config();
config.scaffold = None;
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("Bindings for my-lib"),
"Expected default description, got: {}",
files[0].content
);
assert!(
files[0].content.contains("https://example.invalid/my-lib"),
"Expected vendor-neutral placeholder URL, got: {}",
files[0].content
);
}
#[test]
fn test_capitalize_first_normal() {
assert_eq!(capitalize_first("hello"), "Hello");
}
#[test]
fn test_capitalize_first_empty() {
assert_eq!(capitalize_first(""), "");
}
#[test]
fn test_capitalize_first_already_upper() {
assert_eq!(capitalize_first("World"), "World");
}
#[test]
fn test_extract_code_block_tilde_fence() {
let md = "~~~python\nprint('hi')\n~~~\n";
let result = extract_code_block(md);
assert!(result.contains("~~~python"), "Got: {result}");
assert!(result.contains("print('hi')"), "Got: {result}");
}
#[test]
fn test_include_snippet_non_md_file() {
let tmp = std::env::temp_dir().join("alef_readme_snippet_test_py");
let _ = fs::remove_dir_all(&tmp);
let lang_dir = tmp.join("python");
fs::create_dir_all(&lang_dir).unwrap();
fs::write(lang_dir.join("example.py"), "print('hello')").unwrap();
let result = include_snippet(&tmp, "python", "example.py");
assert!(result.contains("```py"), "Got: {result}");
assert!(result.contains("print('hello')"), "Got: {result}");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_include_snippet_md_file_extracts_code_block() {
let tmp = std::env::temp_dir().join("alef_readme_snippet_test_md");
let _ = fs::remove_dir_all(&tmp);
let lang_dir = tmp.join("python");
fs::create_dir_all(&lang_dir).unwrap();
fs::write(
lang_dir.join("example.md"),
"Some prose\n\n```python\nfoo()\n```\n\nMore prose",
)
.unwrap();
let result = include_snippet(&tmp, "python", "example.md");
assert!(result.contains("```python"), "Got: {result}");
assert!(result.contains("foo()"), "Got: {result}");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_render_performance_table_ops_sec() {
let perf = serde_json::json!({
"platform": "Apple M2",
"function": "parse",
"note": "single-threaded",
"benchmarks": [
{"name": "small.json", "size": "1 KB", "ops_sec": 12345},
{"name": "large.json", "size": "1 MB", "ops_sec": 42}
]
});
let v = Value::from_serialize(&perf);
let result = render_performance_table(&v, "parse");
assert!(result.contains("Apple M2"), "Got: {result}");
assert!(result.contains("| Document | Size | Ops/sec |"), "Got: {result}");
assert!(result.contains("small.json"), "Got: {result}");
assert!(result.contains("large.json"), "Got: {result}");
}
#[test]
fn test_render_performance_table_throughput() {
let perf = serde_json::json!({
"platform": "Linux x86-64",
"function": "extract",
"note": "4 threads",
"benchmarks": [
{
"name": "doc.pdf",
"size": "2 MB",
"latency": "10ms",
"throughput": "100 MB/s"
}
]
});
let v = Value::from_serialize(&perf);
let result = render_performance_table(&v, "extract");
assert!(
result.contains("| Document | Size | Latency | Throughput |"),
"Got: {result}"
);
assert!(result.contains("doc.pdf"), "Got: {result}");
assert!(result.contains("100 MB/s"), "Got: {result}");
assert!(
result.contains("4 threads\n\n| Document"),
"Expected blank line between context and table header. Got: {result}"
);
}
#[test]
fn test_template_with_output_pattern() {
let tmp = std::env::temp_dir().join("alef_readme_test_output_pattern");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("lang.md"), "# {{ name }}").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "lang.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: Some("docs/{language}/README.md".to_string()),
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("docs/python/README.md"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_missing_template_falls_back() {
let tmp = std::env::temp_dir().join("alef_readme_test_missing_tmpl");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "nonexistent.md",
"output_path": "packages/python/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("pip install"),
"Expected hardcoded fallback content, got: {}",
files[0].content
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_no_lang_entry_falls_back() {
let tmp = std::env::temp_dir().join("alef_readme_test_no_lang_entry");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let mut config = test_config();
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: std::collections::HashMap::new(),
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("pip install"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_yaml_config() {
let tmp = std::env::temp_dir().join("alef_readme_test_yaml_cfg");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("tmpl.md"), "version={{ version }}").unwrap();
let yaml_content = r#"
languages:
python:
template: tmpl.md
output_path: packages/python/README.md
"#;
fs::write(tmp.join("readme.yaml"), yaml_content).unwrap();
let mut config = test_config();
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: Some(PathBuf::from("readme.yaml")),
output_pattern: None,
discord_url: None,
banner_url: None,
languages: std::collections::HashMap::new(), });
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("version=0.1.0"),
"Expected rendered version, got: {}",
files[0].content
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_discord_and_banner_url() {
let tmp = std::env::temp_dir().join("alef_readme_test_discord_banner");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("t.md"), "{{ discord_url }}|{{ banner_url }}").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "t.md",
"output_path": "packages/python/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: Some("https://discord.gg/test".to_string()),
banner_url: Some("https://img.example.com/banner.png".to_string()),
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("https://discord.gg/test"),
"Got: {}",
files[0].content
);
assert!(
files[0].content.contains("https://img.example.com/banner.png"),
"Got: {}",
files[0].content
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_no_scaffold_uses_defaults() {
let tmp = std::env::temp_dir().join("alef_readme_test_no_scaffold");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("t.md"), "{{ description }}|{{ repository }}|{{ license }}").unwrap();
let mut config = test_config();
config.scaffold = None;
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "t.md",
"output_path": "packages/python/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("Bindings for my-lib"),
"Got: {}",
files[0].content
);
assert!(
files[0].content.contains("https://example.invalid/my-lib"),
"Got: {}",
files[0].content
);
assert!(files[0].content.contains("MIT"), "Got: {}", files[0].content);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_trailing_newline_not_doubled() {
let tmp = std::env::temp_dir().join("alef_readme_test_trailing_newline");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("t.md"), "hello\n").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "t.md",
"output_path": "packages/python/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].content.ends_with('\n'), "Must end with newline");
assert!(
!files[0].content.ends_with("\n\n"),
"Must not have double trailing newline, got: {:?}",
files[0].content
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_default_readme_path_ffi() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-ffi/README.md"));
}
#[test]
fn test_default_readme_path_wasm() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Wasm]).unwrap();
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-wasm/README.md"));
}
#[test]
fn test_default_readme_path_node() {
let config = test_config();
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Node]).unwrap();
assert_eq!(files[0].path, PathBuf::from("crates/my-lib-node/README.md"));
}
#[test]
fn test_default_readme_path_rust_when_explicitly_configured() {
let mut config = test_config();
let mut readme_cfg = ReadmeConfig {
template_dir: None,
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: std::collections::HashMap::new(),
};
readme_cfg.languages.insert(
"rust".to_string(),
serde_json::json!({ "output_path": "crates/my-lib/README.md" }),
);
config.readme = Some(readme_cfg);
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
assert_eq!(files[0].path, PathBuf::from("crates/my-lib/README.md"));
}
#[test]
fn test_template_output_key_alias() {
let tmp = std::env::temp_dir().join("alef_readme_test_output_alias");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("t.md"), "hello").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "t.md",
"output": "custom/path/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("custom/path/README.md"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_default_path_fallthrough() {
let tmp = std::env::temp_dir().join("alef_readme_test_default_path");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("t.md"), "hello").unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert("python".to_string(), serde_json::json!({ "template": "t.md" }));
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, PathBuf::from("packages/python/README.md"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_readme_missing_snippets_renders_gracefully() {
let tmp = std::env::temp_dir().join("alef_readme_test_missing_snippets");
let _ = fs::remove_dir_all(&tmp);
let partials_dir = tmp.join("partials");
fs::create_dir_all(&partials_dir).unwrap();
fs::write(
partials_dir.join("quick_start.md.jinja"),
"{{ snippets.basic_extraction | include_snippet(language) }}",
)
.unwrap();
fs::write(
tmp.join("language_package.md"),
"{% include 'partials/quick_start.md.jinja' %}",
)
.unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"ffi".to_string(),
serde_json::json!({
"template": "language_package.md",
"output_path": "crates/my-lib-ffi/README.md",
"name": "FFI"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
assert_eq!(files.len(), 1);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_template_include_snippet_filter() {
let tmp = std::env::temp_dir().join("alef_readme_test_snippet_filter");
let _ = fs::remove_dir_all(&tmp);
let snippets_dir = tmp.join("snippets");
let lang_snippet_dir = snippets_dir.join("python");
fs::create_dir_all(&lang_snippet_dir).unwrap();
fs::write(lang_snippet_dir.join("hello.py"), "print('hi')").unwrap();
fs::write(tmp.join("t.md"), r#"{{ "hello.py" | include_snippet("python") }}"#).unwrap();
let mut config = test_config();
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "t.md",
"output_path": "packages/python/README.md"
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(tmp.clone()),
snippets_dir: Some(PathBuf::from("snippets")),
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
config.workspace_root = Some(tmp.clone());
let api = test_api();
let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].content.contains("print('hi')"),
"Expected snippet content, got: {}",
files[0].content
);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_alef_all_and_cold_readme_produce_same_output() {
let tmp = std::env::temp_dir().join("alef_sty5_test");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::create_dir_all(tmp.join("templates")).unwrap();
let template_content = r#"# {{name}}
{{description}}
## Features
- Item 1
- Item 2
{% if performance %}
## Performance
{{ performance | render_performance_table(name) }}
{% endif %}
## Installation
{{ install_command }}
"#;
fs::write(tmp.join("templates/test.md"), template_content).unwrap();
let mut config = test_config();
config.workspace_root = Some(tmp.clone());
let mut lang_map = std::collections::HashMap::new();
lang_map.insert(
"python".to_string(),
serde_json::json!({
"template": "test.md",
"output_path": "packages/python/README.md",
"install_command": "pip install my-lib==0.1.0",
"performance": {
"platform": "Apple M4",
"function": "convert()",
"note": "Test doc",
"benchmarks": [
{
"name": "Small",
"size": "10KB",
"latency": "1.0ms",
"throughput": "10 MB/s"
},
{
"name": "Large",
"size": "1MB",
"latency": "10.0ms",
"throughput": "100 MB/s"
}
]
}
}),
);
config.readme = Some(ReadmeConfig {
template_dir: Some(PathBuf::from("templates")),
snippets_dir: None,
config: None,
output_pattern: None,
discord_url: None,
banner_url: None,
languages: lang_map,
});
let api = test_api();
let cold_files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(cold_files.len(), 1);
let cold_content = &cold_files[0].content;
let warm_files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
assert_eq!(warm_files.len(), 1);
let warm_content = &warm_files[0].content;
if cold_content != warm_content {
eprintln!("=== COLD OUTPUT ===\n{}\n", cold_content);
eprintln!("=== WARM OUTPUT ===\n{}\n", warm_content);
eprintln!("=== DIFF (cold vs warm) ===");
for (i, (c, w)) in cold_content.lines().zip(warm_content.lines()).enumerate() {
if c != w {
eprintln!("Line {}: COLD: {}", i + 1, c);
eprintln!("Line {}: WARM: {}", i + 1, w);
}
}
}
assert_eq!(
cold_content, warm_content,
"README generation must be deterministic: alef readme and alef all must produce identical output (STY-5 regression)"
);
let _ = fs::remove_dir_all(&tmp);
}
}