1use std::collections::BTreeMap;
2
3use 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
13pub struct BadgeInfo {
19 pub key: &'static str,
21 pub official: bool,
23 pub required: &'static [&'static str],
25 pub optional: &'static [&'static str],
27}
28
29pub 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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 "[]\
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 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", "experimental" => "experimental-blue",
255 "looking-for-maintainer" => "looking--for--maintainer-darkblue", "deprecated" => "deprecated-red",
257 _ => "unknown-black",
258 };
259
260 Ok(format!(
262 "",
263 status = status_with_color
264 ))
265}
266
267pub fn crates_io(attrs: Attrs, crate_name: &str) -> Result<String, String> {
268 let name = attrs.get("crate").map(|s| s.as_ref()).unwrap_or(crate_name);
271 Ok(format!(
272 "[](https://crates.io/crates/{name})",
273 name = name
274 ))
275}
276
277fn 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}