Skip to main content

cargo_readme/config/
badges.rs

1use std::collections::BTreeMap;
2
3// https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata
4
5use percent_encoding as pe;
6
7const BADGE_BRANCH_DEFAULT: &str = "master";
8const BADGE_SERVICE_DEFAULT: &str = "github";
9const BADGE_WORKFLOW_DEFAULT: &str = "main";
10
11type Attrs = BTreeMap<String, String>;
12
13/// A badge key cargo-readme understands, plus which attributes it reads.
14///
15/// This is the single source of truth behind `cargo readme --list-badges`. The
16/// `key`s here must stay in sync with the match arms in [`super::manifest::process_badges`];
17/// a unit test in that module enforces it.
18pub struct BadgeInfo {
19    /// The `[badges]` table key, e.g. `"coveralls"`.
20    pub key: &'static str,
21    /// Whether crates.io documents this badge in the manifest reference.
22    pub official: bool,
23    /// Attributes that must be present, or badge rendering fails.
24    pub required: &'static [&'static str],
25    /// Attributes that are read when present, otherwise defaulted.
26    pub optional: &'static [&'static str],
27}
28
29/// Every badge cargo-readme can render, in the order they appear in the output.
30pub const SUPPORTED_BADGES: &[BadgeInfo] = &[
31    BadgeInfo {
32        key: "crates-io",
33        official: false,
34        required: &[],
35        optional: &["crate"],
36    },
37    BadgeInfo {
38        key: "appveyor",
39        official: true,
40        required: &["repository"],
41        optional: &["branch", "service"],
42    },
43    BadgeInfo {
44        key: "circle-ci",
45        official: true,
46        required: &["repository"],
47        optional: &["branch", "service"],
48    },
49    BadgeInfo {
50        key: "gitlab",
51        official: true,
52        required: &["repository"],
53        optional: &["branch"],
54    },
55    BadgeInfo {
56        key: "travis-ci",
57        official: true,
58        required: &["repository"],
59        optional: &["branch"],
60    },
61    BadgeInfo {
62        key: "github",
63        official: false,
64        required: &["repository"],
65        optional: &["workflow"],
66    },
67    BadgeInfo {
68        key: "codecov",
69        official: true,
70        required: &["repository"],
71        optional: &["branch", "service"],
72    },
73    BadgeInfo {
74        key: "coveralls",
75        official: true,
76        required: &["repository"],
77        optional: &["branch", "service"],
78    },
79    BadgeInfo {
80        key: "is-it-maintained-issue-resolution",
81        official: true,
82        required: &["repository"],
83        optional: &[],
84    },
85    BadgeInfo {
86        key: "is-it-maintained-open-issues",
87        official: true,
88        required: &["repository"],
89        optional: &[],
90    },
91    BadgeInfo {
92        key: "maintenance",
93        official: true,
94        required: &["status"],
95        optional: &[],
96    },
97];
98
99pub fn appveyor(attrs: Attrs) -> Result<String, String> {
100    let repo = required(&attrs, "appveyor", "repository")?;
101    let branch = attrs
102        .get("branch")
103        .map(|i| i.as_ref())
104        .unwrap_or(BADGE_BRANCH_DEFAULT);
105    let service = attrs
106        .get("service")
107        .map(|i| i.as_ref())
108        .unwrap_or(BADGE_SERVICE_DEFAULT);
109
110    Ok(format!(
111        "[![Build Status](https://ci.appveyor.com/api/projects/status/{service}/{repo}?branch={branch}&svg=true)]\
112        (https://ci.appveyor.com/project/{repo}/branch/{branch})",
113        repo=repo, branch=branch, service=service
114    ))
115}
116
117pub fn circle_ci(attrs: Attrs) -> Result<String, String> {
118    let repo = required(&attrs, "circle-ci", "repository")?;
119    let branch = attrs
120        .get("branch")
121        .map(|i| i.as_ref())
122        .unwrap_or(BADGE_BRANCH_DEFAULT);
123    let service = badge_service_short_name(
124        attrs
125            .get("service")
126            .map(|i| i.as_ref())
127            .unwrap_or(BADGE_SERVICE_DEFAULT),
128    );
129
130    Ok(format!(
131        "[![Build Status](https://circleci.com/{service}/{repo}/tree/{branch}.svg?style=shield)]\
132         (https://circleci.com/{service}/{repo}/tree/{branch})",
133        repo = repo,
134        service = service,
135        branch = percent_encode(branch)
136    ))
137}
138
139pub fn gitlab(attrs: Attrs) -> Result<String, String> {
140    let repo = required(&attrs, "gitlab", "repository")?;
141    let branch = attrs
142        .get("branch")
143        .map(|i| i.as_ref())
144        .unwrap_or(BADGE_BRANCH_DEFAULT);
145
146    Ok(format!(
147        "[![Build Status](https://gitlab.com/{repo}/badges/{branch}/pipeline.svg)]\
148         (https://gitlab.com/{repo}/commits/master)",
149        repo = repo,
150        branch = percent_encode(branch)
151    ))
152}
153
154pub fn travis_ci(attrs: Attrs) -> Result<String, String> {
155    let repo = required(&attrs, "travis-ci", "repository")?;
156    let branch = attrs
157        .get("branch")
158        .map(|i| i.as_ref())
159        .unwrap_or(BADGE_BRANCH_DEFAULT);
160
161    Ok(format!(
162        "[![Build Status](https://travis-ci.org/{repo}.svg?branch={branch})]\
163         (https://travis-ci.org/{repo})",
164        repo = repo,
165        branch = percent_encode(branch)
166    ))
167}
168
169pub fn github(attrs: Attrs) -> Result<String, String> {
170    let repo = required(&attrs, "github", "repository")?;
171    let workflow = attrs
172        .get("workflow")
173        .map(|i| i.as_ref())
174        .unwrap_or(BADGE_WORKFLOW_DEFAULT);
175
176    Ok(format!(
177        "[![Workflow Status](https://github.com/{repo}/workflows/{workflow}/badge.svg)]\
178         (https://github.com/{repo}/actions?query=workflow%3A%22{workflow_plus}%22)",
179        repo = repo,
180        workflow = percent_encode(workflow),
181        workflow_plus = percent_encode(&str::replace(workflow, " ", "+"))
182    ))
183}
184
185pub fn codecov(attrs: Attrs) -> Result<String, String> {
186    let repo = required(&attrs, "codecov", "repository")?;
187    let branch = attrs
188        .get("branch")
189        .map(|i| i.as_ref())
190        .unwrap_or(BADGE_BRANCH_DEFAULT);
191    let service = badge_service_short_name(
192        attrs
193            .get("service")
194            .map(|i| i.as_ref())
195            .unwrap_or(BADGE_SERVICE_DEFAULT),
196    );
197
198    Ok(format!(
199        "[![Coverage Status](https://codecov.io/{service}/{repo}/branch/{branch}/graph/badge.svg)]\
200         (https://codecov.io/{service}/{repo})",
201        repo = repo,
202        branch = percent_encode(branch),
203        service = service
204    ))
205}
206
207pub fn coveralls(attrs: Attrs) -> Result<String, String> {
208    let repo = required(&attrs, "coveralls", "repository")?;
209    let branch = attrs
210        .get("branch")
211        .map(|i| i.as_ref())
212        .unwrap_or(BADGE_BRANCH_DEFAULT);
213    let service = attrs
214        .get("service")
215        .map(|i| i.as_ref())
216        .unwrap_or(BADGE_SERVICE_DEFAULT);
217
218    Ok(format!(
219        "[![Coverage Status](https://coveralls.io/repos/{service}/{repo}/badge.svg?branch={branch})]\
220         (https://coveralls.io/{service}/{repo}?branch={branch})",
221        repo = repo,
222        branch = percent_encode(branch),
223        service = service
224    ))
225}
226
227pub fn is_it_maintained_issue_resolution(attrs: Attrs) -> Result<String, String> {
228    let repo = required(&attrs, "is-it-maintained-issue-resolution", "repository")?;
229    Ok(format!(
230        "[![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/{repo}.svg)]\
231        (https://isitmaintained.com/project/{repo} \"Average time to resolve an issue\")",
232        repo=repo
233    ))
234}
235
236pub fn is_it_maintained_open_issues(attrs: Attrs) -> Result<String, String> {
237    let repo = required(&attrs, "is-it-maintained-open-issues", "repository")?;
238    Ok(format!(
239        "[![Percentage of issues still open](https://isitmaintained.com/badge/open/{repo}.svg)]\
240         (https://isitmaintained.com/project/{repo} \"Percentage of issues still open\")",
241        repo = repo
242    ))
243}
244
245pub fn maintenance(attrs: Attrs) -> Result<String, String> {
246    let status = required(&attrs, "maintenance", "status")?;
247
248    // https://github.com/rust-lang/crates.io/blob/5a08887d4b531e034d01386d3e5997514f3c8ee5/src/models/badge.rs#L82
249    let status_with_color = match status {
250        "actively-developed" => "actively--developed-brightgreen",
251        "passively-maintained" => "passively--maintained-yellowgreen",
252        "as-is" => "as--is-yellow",
253        "none" => "maintenance-none-lightgrey", // color is a guess
254        "experimental" => "experimental-blue",
255        "looking-for-maintainer" => "looking--for--maintainer-darkblue", // color is a guess
256        "deprecated" => "deprecated-red",
257        _ => "unknown-black",
258    };
259
260    //example https://img.shields.io/badge/maintenance-experimental-blue.svg
261    Ok(format!(
262        "![Maintenance](https://img.shields.io/badge/maintenance-{status}.svg)",
263        status = status_with_color
264    ))
265}
266
267pub fn crates_io(attrs: Attrs, crate_name: &str) -> Result<String, String> {
268    // Not part of the official `[badges]` set, but the crate name is already in
269    // `Cargo.toml`, so `crate` defaults to the package name when omitted.
270    let name = attrs.get("crate").map(|s| s.as_ref()).unwrap_or(crate_name);
271    Ok(format!(
272        "[![Crates.io](https://img.shields.io/crates/v/{name}.svg)](https://crates.io/crates/{name})",
273        name = name
274    ))
275}
276
277/// Look up an attribute that a badge cannot render without.
278fn required<'a>(attrs: &'a Attrs, badge: &str, key: &str) -> Result<&'a str, String> {
279    attrs
280        .get(key)
281        .map(String::as_str)
282        .ok_or_else(|| format!("badge `{badge}` is missing required attribute `{key}`"))
283}
284
285fn percent_encode(input: &str) -> pe::PercentEncode<'_> {
286    pe::utf8_percent_encode(input, pe::NON_ALPHANUMERIC)
287}
288
289fn badge_service_short_name(service: &str) -> &'static str {
290    match service {
291        "github" => "gh",
292        "bitbucket" => "bb",
293        "gitlab" => "gl",
294        _ => "gh",
295    }
296}