use crate::Config;
use crate::LocalStarterGroupLookup;
use crate::config::resolve_instance_path;
use crate::starter::get_starter_command;
use crate::starter::parse_starters;
use anyhow::Result;
use std::env;
use std::fs;
use std::path::Path;
fn generate_readme_section(groups: &LocalStarterGroupLookup) -> String {
let mut output: Vec<String> = Vec::new();
output.push("\n<!-- NOTE: The starters section of this readme is auto-generated by .github/workflows/deploy.yml -->".to_string());
output.push("\nJump to group:".to_string());
let mut sorted_groups: Vec<_> = groups.iter().collect();
sorted_groups.sort_by_key(|(key, _)| *key);
for (group_name, _) in &sorted_groups {
output.push(format!("- [{}](#{})", group_name, group_name));
}
output.push("\n---\n".to_string());
for (group_name, group_data) in &sorted_groups {
output.push(format!("### {}\n", group_name));
let mut sorted_data: Vec<_> = group_data.iter().collect();
sorted_data.sort_by_key(|data| &data.name);
for data in sorted_data {
output.push(format!("{}/**{}**", data.group, data.name));
let github_username = env::var("GITHUB_USERNAME").unwrap_or_default();
let github_repo = env::var("GITHUB_REPO").unwrap_or_default();
output.push(format!(
"
```
{}
```
",
get_starter_command(data, &github_username, &github_repo)
));
if let Some(config) = &data.config {
if let Some(description) = &config.description {
if !description.is_empty() {
output.push(description.clone());
}
}
}
output.push("---".to_string());
}
}
output.join("\n")
}
fn rewrite_readme_section(existing_content: &str, section: &str, new_content: &str) -> String {
let lines: Vec<&str> = existing_content.split('\n').collect();
let mut rewritten_lines: Vec<String> = Vec::new();
let mut is_in_section = false;
for line in lines {
if line == section {
is_in_section = true;
rewritten_lines.push(line.to_string());
rewritten_lines.push("".to_string());
rewritten_lines.push(new_content.to_string());
rewritten_lines.push("".to_string());
} else {
if line.starts_with("## ") {
is_in_section = false;
}
if !is_in_section {
rewritten_lines.push(line.to_string());
}
}
}
rewritten_lines.join("\n")
}
pub fn update_readme(config: Config, instance_path: Option<&str>) -> Result<()> {
let path = resolve_instance_path(&config, instance_path);
println!("Using instance at {:?}", path);
let groups = parse_starters(&path)?;
let starters_section = generate_readme_section(&groups);
let readme_path = Path::new(&path).join("README.md");
let existing_readme = fs::read_to_string(&readme_path)
.map_err(|e| anyhow::anyhow!("Failed to read README.md: {}", e))?;
let updated_readme = rewrite_readme_section(&existing_readme, "## Starters", &starters_section);
fs::write(&readme_path, updated_readme)
.map_err(|e| anyhow::anyhow!("Failed to write updated README.md: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LocalStarter;
use crate::starter::StarterConfig;
use std::collections::HashMap;
fn create_test_starter(group: &str, name: &str, description: Option<&str>) -> LocalStarter {
let config = if let Some(desc) = description {
Some(StarterConfig {
description: Some(desc.to_string()),
default_dir: None,
main_file: None,
preview: None,
})
} else {
None
};
LocalStarter {
path: format!("{}/{}", group, name),
group: group.to_string(),
name: name.to_string(),
config,
}
}
#[test]
fn test_generate_readme_section_empty() {
let groups = HashMap::new();
let result = generate_readme_section(&groups);
assert!(result.contains("auto-generated by .github/workflows/deploy.yml"));
assert!(result.contains("Jump to group:"));
assert!(!result.contains("###"));
}
#[test]
fn test_generate_readme_section_single_group() {
let mut groups = HashMap::new();
let starter = create_test_starter("react", "basic-app", Some("A React starter"));
groups.insert("react".to_string(), vec![starter]);
let result = generate_readme_section(&groups);
assert!(result.contains("- [react](#react)"));
assert!(result.contains("### react"));
assert!(result.contains("react/**basic-app**"));
assert!(result.contains("A React starter"));
}
#[test]
fn test_generate_readme_section_multiple_groups_sorted() {
let mut groups = HashMap::new();
let react_starter = create_test_starter("react", "app", Some("React app"));
let node_starter = create_test_starter("node", "server", Some("Node server"));
let angular_starter = create_test_starter("angular", "component", None);
groups.insert("react".to_string(), vec![react_starter]);
groups.insert("node".to_string(), vec![node_starter]);
groups.insert("angular".to_string(), vec![angular_starter]);
let result = generate_readme_section(&groups);
let angular_pos = result.find("- [angular](#angular)").unwrap();
let node_pos = result.find("- [node](#node)").unwrap();
let react_pos = result.find("- [react](#react)").unwrap();
assert!(angular_pos < node_pos);
assert!(node_pos < react_pos);
}
#[test]
fn test_generate_readme_section_multiple_starters_in_group_sorted() {
let mut groups = HashMap::new();
let starter1 = create_test_starter("react", "zebra-app", Some("Z starter"));
let starter2 = create_test_starter("react", "alpha-app", Some("A starter"));
let starter3 = create_test_starter("react", "beta-app", Some("B starter"));
groups.insert("react".to_string(), vec![starter1, starter2, starter3]);
let result = generate_readme_section(&groups);
let alpha_pos = result.find("react/**alpha-app**").unwrap();
let beta_pos = result.find("react/**beta-app**").unwrap();
let zebra_pos = result.find("react/**zebra-app**").unwrap();
assert!(alpha_pos < beta_pos);
assert!(beta_pos < zebra_pos);
}
#[test]
fn test_generate_readme_section_with_command() {
use crate::starter::get_starter_command;
let mut groups = HashMap::new();
let starter = create_test_starter("react", "app", Some("Test starter"));
groups.insert("react".to_string(), vec![starter.clone()]);
let jump_start_command = get_starter_command(&starter, "testuser", "testrepo");
assert_eq!(jump_start_command, "jump-start use @testuser/testrepo/react/app");
let default_repo_command = get_starter_command(&starter, "testuser", "jump-start");
assert_eq!(default_repo_command, "jump-start use @testuser/react/app");
let local_command = get_starter_command(&starter, "", "");
assert_eq!(local_command, "jump-start use react/app");
}
#[test]
fn test_generate_readme_section_default_command() {
let mut groups = HashMap::new();
let starter = create_test_starter("react", "app", Some("Test starter"));
groups.insert("react".to_string(), vec![starter]);
let result = generate_readme_section(&groups);
assert!(result.contains("jump-start use react/app"));
}
#[test]
fn test_generate_readme_section_no_description() {
let mut groups = HashMap::new();
let starter = create_test_starter("react", "app", None);
groups.insert("react".to_string(), vec![starter]);
let result = generate_readme_section(&groups);
assert!(result.contains("react/**app**"));
assert!(result.contains("jump-start use react/app"));
assert!(result.contains("react/**app**"));
assert!(result.contains("jump-start use react/app"));
assert!(result.contains("---"));
}
#[test]
fn test_rewrite_readme_section_basic() {
let existing_content = r#"# Test README
## Installation
Install instructions here.
## Starters
Old starters content
that spans multiple lines.
## Contributing
Contributing guidelines.
"#;
let new_content = "New starters content\nwith multiple lines.";
let result = rewrite_readme_section(existing_content, "## Starters", new_content);
assert!(result.contains("# Test README"));
assert!(result.contains("## Installation"));
assert!(result.contains("Install instructions here"));
assert!(result.contains("## Contributing"));
assert!(result.contains("Contributing guidelines"));
assert!(result.contains("New starters content\nwith multiple lines."));
assert!(!result.contains("Old starters content"));
}
#[test]
fn test_rewrite_readme_section_no_existing_section() {
let existing_content = r#"# Test README
## Installation
Install instructions here.
## Contributing
Contributing guidelines.
"#;
let new_content = "New starters content";
let result = rewrite_readme_section(existing_content, "## Starters", new_content);
assert_eq!(result, existing_content);
}
#[test]
fn test_rewrite_readme_section_multiple_sections() {
let existing_content = r#"# Test README
## Installation
Install instructions.
## Starters
Old starters content.
## Usage
Usage instructions.
## API
API documentation.
"#;
let new_content = "Updated starters content.";
let result = rewrite_readme_section(existing_content, "## Starters", new_content);
assert!(result.contains("## Installation"));
assert!(result.contains("Install instructions"));
assert!(result.contains("## Usage"));
assert!(result.contains("Usage instructions"));
assert!(result.contains("## API"));
assert!(result.contains("API documentation"));
assert!(result.contains("Updated starters content."));
assert!(!result.contains("Old starters content"));
}
#[test]
fn test_rewrite_readme_section_preserves_empty_lines() {
let existing_content = r#"# Test README
## Installation
Install instructions.
## Starters
Old content.
## Contributing
Contributing.
"#;
let new_content = "New content.";
let result = rewrite_readme_section(existing_content, "## Starters", new_content);
assert!(result.contains("# Test README"));
assert!(result.contains("## Installation"));
assert!(result.contains("Install instructions"));
assert!(result.contains("## Starters"));
assert!(result.contains("New content."));
assert!(result.contains("## Contributing"));
}
}