1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum Severity {
9 Critical,
10 Warning,
11 Notice,
12}
13
14impl Severity {
15 pub fn from_slug(slug: &str) -> Option<Severity> {
18 match slug {
19 "critical" => Some(Severity::Critical),
20 "warning" => Some(Severity::Warning),
21 "notice" => Some(Severity::Notice),
22 _ => None,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct IssueBits(pub u64);
30
31macro_rules! check_ids {
32 ($($variant:ident = $id:literal => $slug:literal,)+) => {
33 #[repr(u8)]
36 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
37 pub enum CheckId {
38 $(
39 #[serde(rename = $slug)]
40 $variant = $id,
41 )+
42 }
43
44 impl CheckId {
45 pub const ALL: [CheckId; { [$($slug),+].len() }] = [$(CheckId::$variant),+];
47
48 pub fn slug(self) -> &'static str {
50 match self {
51 $(CheckId::$variant => $slug,)+
52 }
53 }
54 }
55 };
56}
57
58check_ids! {
59 Http4xx = 0 => "http_4xx",
60 Http5xx = 1 => "http_5xx",
61 FetchFailed = 2 => "fetch_failed",
62 RedirectLoop = 3 => "redirect_loop",
63 Redirected = 4 => "redirected",
64 RedirectChain = 5 => "redirect_chain",
65 Noindex = 6 => "noindex",
66 Canonicalised = 7 => "canonicalised",
67 CanonicalMissing = 8 => "canonical_missing",
68 BlockedByRobots = 9 => "blocked_by_robots",
69 CanonicalToNon200 = 10 => "canonical_to_non_200",
70 RobotsBlocksSite = 11 => "robots_blocks_site",
71 TitleMissing = 12 => "title_missing",
72 TitleTooLong = 13 => "title_too_long",
73 TitleTooShort = 14 => "title_too_short",
74 TitleMultiple = 15 => "title_multiple",
75 TitleDuplicate = 16 => "title_duplicate",
76 DescriptionMissing = 17 => "description_missing",
77 DescriptionTooLong = 18 => "description_too_long",
78 DescriptionTooShort = 19 => "description_too_short",
79 DescriptionDuplicate = 20 => "description_duplicate",
80 H1Missing = 21 => "h1_missing",
81 H1Multiple = 22 => "h1_multiple",
82 H1Duplicate = 23 => "h1_duplicate",
83 ThinContent = 24 => "thin_content",
84 ContentDuplicate = 25 => "content_duplicate",
85 ImagesMissingAlt = 26 => "images_missing_alt",
86 LinksToBroken = 27 => "links_to_broken",
87 LinksToRedirect = 28 => "links_to_redirect",
88 Orphan = 29 => "orphan",
89 NoInternalOutlinks = 30 => "no_internal_outlinks",
90 NofollowInternalLinks = 31 => "nofollow_internal_links",
91 DeepPage = 32 => "deep_page",
92 SitemapNon200 = 33 => "sitemap_non_200",
93 SitemapNoindex = 34 => "sitemap_noindex",
94 SitemapCanonicalised = 35 => "sitemap_canonicalised",
95 NotInSitemap = 36 => "not_in_sitemap",
96 SitemapMissing = 37 => "sitemap_missing",
97 MixedContent = 38 => "mixed_content",
98 NotHttps = 39 => "not_https",
99 SlowResponse = 40 => "slow_response",
100 OgMissing = 41 => "og_missing",
101 JsonldInvalid = 42 => "jsonld_invalid",
102 HreflangMissingSelf = 43 => "hreflang_missing_self",
103}
104
105const _: () = {
109 let mut i = 0;
110 while i < CheckId::ALL.len() {
111 assert!((CheckId::ALL[i] as u8) < 64, "check ID is 64 or more");
112 if i > 0 {
113 assert!(
114 (CheckId::ALL[i - 1] as u8) < (CheckId::ALL[i] as u8),
115 "check IDs must be unique and in ascending order"
116 );
117 }
118 i += 1;
119 }
120};
121
122impl CheckId {
123 pub const fn bit(self) -> u8 {
125 self as u8
126 }
127
128 pub fn from_bit(bit: u8) -> Option<CheckId> {
129 CheckId::ALL.into_iter().find(|c| c.bit() == bit)
130 }
131
132 pub fn from_slug(slug: &str) -> Option<CheckId> {
134 CheckId::ALL.into_iter().find(|c| c.slug() == slug)
135 }
136}
137
138impl IssueBits {
139 pub fn set_check(&mut self, id: CheckId) {
140 self.set(id.bit());
141 }
142
143 pub fn has_check(&self, id: CheckId) -> bool {
144 self.has(id.bit())
145 }
146
147 pub fn set(&mut self, bit: u8) {
148 assert!(bit < 64, "check bit {bit} out of range");
149 self.0 |= 1 << bit;
150 }
151
152 pub fn has(&self, bit: u8) -> bool {
153 bit < 64 && self.0 & (1 << bit) != 0
154 }
155
156 pub fn iter(&self) -> impl Iterator<Item = u8> + '_ {
157 (0..64u8).filter(|b| self.has(*b))
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn issue_bits_set_and_test() {
167 let mut b = IssueBits::default();
168 b.set(0);
169 b.set(63);
170 assert!(b.has(0));
171 assert!(b.has(63));
172 assert!(!b.has(1));
173 assert_eq!(b.iter().collect::<Vec<_>>(), vec![0, 63]);
174 }
175
176 #[test]
177 #[should_panic(expected = "check bit 64 out of range")]
178 fn issue_bit_64_is_rejected() {
179 IssueBits::default().set(64);
180 }
181}