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