Skip to main content

wowlab_docgen_cli/infra/
badge.rs

1// #t(file: rust_hardcoded_url) badge presets use shields.io URLs by design
2
3const DEFAULT_REPO: &str = "legacy3/wowlab";
4
5/// Build shields.io badge markdown for the given preset names.
6#[must_use]
7pub fn build(workspace: &crate::WorkspaceIndex, names: &[String]) -> String {
8    let repo = detect_repo(workspace);
9
10    names
11        .iter()
12        .filter_map(|name| render_badge(name, workspace, &repo))
13        .collect::<Vec<_>>()
14        .join("\n  ")
15}
16
17fn detect_repo(workspace: &crate::WorkspaceIndex) -> String {
18    let cargo_path = workspace.root().join("Cargo.toml");
19    let Some(content) = workspace.contents(&cargo_path) else {
20        return DEFAULT_REPO.to_string();
21    };
22
23    for line in content.lines() {
24        let trimmed = line.trim();
25
26        if let Some(rest) = trimmed.strip_prefix("repository") {
27            let rest = rest.trim().strip_prefix('=').unwrap_or(rest);
28            let rest = rest.trim().trim_matches('"');
29
30            if let Some(gh) = rest.strip_prefix("https://github.com/") {
31                let slug = gh.trim_end_matches('/');
32
33                // #t(block: rust_alloc_in_loop) badge presets are a small fixed set
34                if !slug.is_empty() {
35                    return slug.to_string();
36                }
37            }
38        }
39    }
40
41    DEFAULT_REPO.to_string()
42}
43
44fn render_badge(name: &str, workspace: &crate::WorkspaceIndex, repo: &str) -> Option<String> {
45    match name {
46        "ci" => {
47            if !workspace.contains(&workspace.root().join(".github/workflows/ci.yml")) {
48                return None;
49            }
50
51            Some(format!(
52                r#"<a href="https://github.com/{repo}/actions/workflows/ci.yml"><img src="https://github.com/{repo}/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>"#
53            ))
54        }
55        "discord" => Some(
56            r#"<a href="https://wowlab.gg/go/discord"><img src="https://img.shields.io/badge/discord-join-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>"#.to_string(),
57        ),
58        "license" => Some(
59            r#"<a href="LICENSE.md"><img src="https://img.shields.io/badge/license-PolyForm%20Noncommercial-green" alt="License" /></a>"#.to_string(),
60        ),
61        "website" => Some(
62            r#"<a href="https://wowlab.gg"><img src="https://img.shields.io/badge/website-wowlab.gg-blue" alt="Website" /></a>"#.to_string(),
63        ),
64        _ => None,
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use googletest::prelude::*;
71    use wowlab_fs::path::Path;
72
73    use super::*;
74    use crate::{WorkspaceFile, WorkspaceIndex};
75
76    fn workspace(files: &[(&str, &str)]) -> WorkspaceIndex {
77        let root = Path::new("/ws");
78        let files = files
79            .iter()
80            .map(|(path, contents)| WorkspaceFile {
81                path: root.join(path),
82                contents: Some(Box::from(*contents)),
83            })
84            .collect();
85
86        WorkspaceIndex::new(root.to_path_buf(), files)
87    }
88
89    fn build(files: &[(&str, &str)], names: &[String]) -> String {
90        super::build(&workspace(files), names)
91    }
92
93    fn detect_repo(files: &[(&str, &str)]) -> String {
94        super::detect_repo(&workspace(files))
95    }
96
97    #[gtest]
98    fn build_static_badges() -> Result<()> {
99        let result = build(
100            &[],
101            &[
102                "discord".to_string(),
103                "license".to_string(),
104                "website".to_string(),
105            ],
106        );
107
108        verify_that!(result.as_str(), contains_substring("alt=\"Discord\""))?;
109        verify_that!(result.as_str(), contains_substring("alt=\"License\""))?;
110        verify_that!(result.as_str(), contains_substring("alt=\"Website\""))?;
111
112        Ok(())
113    }
114
115    #[gtest]
116    fn build_ci_badge_requires_workflow() -> Result<()> {
117        let result = build(&[], &["ci".to_string()]);
118
119        verify_eq!(result, "")?;
120
121        let result = build(
122            &[(".github/workflows/ci.yml", "name: CI")],
123            &["ci".to_string()],
124        );
125
126        verify_that!(
127            result.as_str(),
128            contains_substring("actions/workflows/ci.yml/badge.svg")
129        )?;
130
131        Ok(())
132    }
133
134    #[gtest]
135    fn build_skips_unknown_names() -> Result<()> {
136        let result = build(
137            &[],
138            &[
139                "discord".to_string(),
140                "bogus".to_string(),
141                "license".to_string(),
142            ],
143        );
144
145        verify_eq!(result.lines().count(), 2)
146    }
147
148    #[gtest]
149    fn build_empty_for_no_names() -> Result<()> {
150        verify_eq!(build(&[], &[]), "")
151    }
152
153    #[gtest]
154    fn detect_repo_from_cargo_toml() -> Result<()> {
155        verify_eq!(
156            detect_repo(&[(
157                "Cargo.toml",
158                "[workspace.package]\nrepository = \"https://github.com/myorg/myrepo\"\n",
159            )]),
160            "myorg/myrepo"
161        )
162    }
163
164    #[gtest]
165    fn detect_repo_fallback() -> Result<()> {
166        verify_eq!(detect_repo(&[]), DEFAULT_REPO)
167    }
168
169    #[gtest]
170    fn ci_badge_uses_detected_repo() -> Result<()> {
171        let result = build(
172            &[
173                (".github/workflows/ci.yml", "name: CI"),
174                (
175                    "Cargo.toml",
176                    "[workspace.package]\nrepository = \"https://github.com/cool/project\"\n",
177                ),
178            ],
179            &["ci".to_string()],
180        );
181
182        verify_that!(result.as_str(), contains_substring("cool/project"))
183    }
184}