1#![warn(missing_docs)]
4#![warn(clippy::all)]
5#![warn(clippy::pedantic)]
6#![warn(clippy::nursery)]
7#![warn(clippy::cargo)]
8
9const LINK: &str = "https://datatracker.ietf.org/doc/html/rfc";
10
11#[derive(Debug, Clone, Copy)]
12enum RfcStatus {
13 Unknown, Draft, Informational, Experimental, BestCommonPractice, ProposedStandard, DraftStandard, InternetStandard, Historic, Obsolete, }
24
25impl Default for RfcStatus {
26 fn default() -> Self {
27 Self::Unknown
28 }
29}
30
31impl RfcStatus {
32 const fn as_color(self) -> &'static str {
33 match self {
34 Self::Unknown => "#F0F0F0",
35 Self::Draft => "#F04040",
36 Self::Informational => "#F0A000",
37 Self::Experimental => "#E0E000",
38 Self::BestCommonPractice => "#F040F0",
39 Self::ProposedStandard => "#6060F0",
40 Self::DraftStandard => "#40D0D0",
41 Self::InternetStandard => "#40F040",
42 Self::Historic => "#606060",
43 Self::Obsolete => "#804000",
44 }
45 }
46
47 fn from_classes(classes: Vec<&str>) -> Option<Self> {
48 for i in classes {
49 let found = match i {
50 "bgwhite" => Some(Self::Unknown),
51 "bgred" => Some(Self::Draft),
52 "bggrey" => Some(Self::Historic),
53 "bgbrown" => Some(Self::Obsolete),
54 "bgorange" => Some(Self::Informational),
55 "bgyellow" => Some(Self::Experimental),
56 "bgmagenta" => Some(Self::BestCommonPractice),
57 "bgblue" => Some(Self::ProposedStandard),
58 "bgcyan" => Some(Self::DraftStandard),
59 "bggreen" => Some(Self::InternetStandard),
60 _ => None,
61 };
62 if found.is_some() {
63 return found;
64 }
65 }
66 None
67 }
68}
69
70pub struct RfcGraph {
74 did_search:
75 std::collections::HashMap<i32, (bool, petgraph::prelude::NodeIndex<u32>, RfcStatus)>,
76 graph: petgraph::Graph<i32, i32>,
77 cache: std::collections::HashMap<i32, Vec<i32>>,
78}
79
80impl Default for RfcGraph {
82 fn default() -> Self {
83 Self {
84 did_search: std::collections::HashMap::default(),
85 graph: petgraph::Graph::default(),
86 cache: std::fs::read("cache.json")
87 .map_err(anyhow::Error::msg)
88 .and_then(|file| serde_json::from_slice(&file).map_err(anyhow::Error::msg))
89 .unwrap_or_default(),
90 }
91 }
92}
93
94impl Drop for RfcGraph {
96 fn drop(&mut self) {
97 serde_json::to_string_pretty(&self.cache)
98 .map_err(anyhow::Error::msg)
99 .and_then(|json| std::fs::write("cache.json", json).map_err(anyhow::Error::msg))
100 .unwrap();
101 }
102}
103
104impl RfcGraph {
105 async fn query_list_links_of_rfc(&mut self, number: i32) -> (Vec<i32>, RfcStatus) {
106 let html = reqwest::get(format!("{LINK}{number}"))
111 .await
112 .unwrap()
113 .text()
114 .await
115 .unwrap();
116
117 let document = scraper::Html::parse_document(&html);
118 let selector = scraper::Selector::parse("a").unwrap();
119
120 let mut links = document
121 .select(&selector)
122 .filter_map(|i| {
123 let href = match i.value().attr("href") {
124 Some(value) => value,
125 None => return None,
126 };
127
128 if !href.starts_with("/doc/html/rfc") {
129 return None;
130 }
131
132 Some(href)
133 })
134 .collect::<Vec<_>>();
135
136 links.sort_unstable();
137 links.dedup();
138
139 let mut links = links
140 .into_iter()
141 .filter_map(|i| i.strip_prefix("/doc/html/rfc")?.parse::<i32>().ok())
142 .collect::<Vec<_>>();
143
144 links.sort_unstable();
145 links.dedup();
146
147 let links = links
148 .into_iter()
149 .filter(|i| *i != number)
150 .collect::<Vec<_>>();
151
152 let selector =
153 scraper::Selector::parse(r#"div[title="Click for colour legend."]"#).unwrap();
154
155 let html_color = document.select(&selector).next().unwrap();
156 let html_color_classes =
157 RfcStatus::from_classes(html_color.value().classes().collect::<Vec<_>>()).unwrap();
158 println!("{html_color_classes:?}");
159
160 self.cache.insert(number, links.clone());
161 (links, html_color_classes)
162 }
163}
164
165impl RfcGraph {
166 fn get_or_emplace(
167 &mut self,
168 number: i32,
169 search: bool,
170 color: Option<RfcStatus>,
171 ) -> petgraph::prelude::NodeIndex<u32> {
172 if let Some((_, i, status)) = self.did_search.get_mut(&number) {
173 if let Some(color) = color {
174 *status = color;
175 }
176 *i
177 } else {
178 let node = self.graph.add_node(number);
179 self.did_search
180 .insert(number, (search, node, color.unwrap_or(RfcStatus::Unknown)));
181 node
182 }
183 }
184
185 async fn add(&mut self, number: i32) -> Option<(Vec<i32>, RfcStatus)> {
186 if self.did_search.get(&number).map_or(false, |i| i.0) {
187 return None;
188 }
189 let (linked, color) = self.query_list_links_of_rfc(number).await;
190 let number_node = self.get_or_emplace(number, true, Some(color));
191
192 let nodes_linked = linked
193 .iter()
194 .map(|i| (number_node, self.get_or_emplace(*i, false, None)))
195 .collect::<Vec<_>>();
196
197 self.graph.extend_with_edges(nodes_linked);
198 self.to_svg();
199 Some((linked, color))
200 }
201
202 fn to_svg(&self) {
203 let mut file = std::fs::OpenOptions::new()
204 .create(true)
205 .write(true)
206 .truncate(true)
207 .open("input.dot")
208 .unwrap();
209
210 std::io::Write::write_fmt(
211 &mut file,
212 format_args!(
213 "{:?}",
214 petgraph::dot::Dot::with_attr_getters(
215 &self.graph,
216 &[petgraph::dot::Config::EdgeNoLabel],
217 &|_, _| String::new(),
218 &|_, node| {
219 let (_, _, color) = self.did_search.get(node.1).unwrap();
220
221 format!(
222 "color=\"{color}\" style=\"filled\"",
223 color = color.as_color()
224 )
225 }
226 )
227 ),
228 )
229 .unwrap();
230
231 std::process::Command::new("dot")
232 .arg("-Tsvg")
233 .arg("input.dot")
234 .arg("-o")
235 .arg("output.svg")
236 .output()
237 .unwrap();
238 }
239}
240
241impl RfcGraph {
242 #[async_recursion::async_recursion]
243 async fn rec_get_rfc<'a>(&'a mut self, number: i32, rec_max: u32) -> Vec<i32> {
244 let mut output = vec![];
246 if rec_max != 0 {
247 let (linked, _) = self.add(number).await.unwrap_or_default();
248 output.extend(&linked);
249 for i in linked {
250 output.extend(self.rec_get_rfc(i, rec_max - 1).await);
251 }
252 }
253 output
254 }
255
256 pub async fn get(root: i32, recursion_max: u32) -> Vec<i32> {
260 Self::default().rec_get_rfc(root, recursion_max).await
261 }
262}