rfc_graph/
lib.rs

1//! Fast and easy queue abstraction.
2
3#![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,            // #FFF
14    Draft,              // #F44
15    Informational,      // #FA0
16    Experimental,       // #EE0
17    BestCommonPractice, // #F4F
18    ProposedStandard,   // #66F
19    DraftStandard,      // #4DD
20    InternetStandard,   // #4F4
21    Historic,           // #666
22    Obsolete,           // #840
23}
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
70/// The `RfcGraph` type, wrapping all the logics of this crate.
71///
72/// Use the function [`RfcGraph::get`]
73pub 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
80/// Will initialize a graph model, and load a `cache.json` file to reduce web query.
81impl 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
94/// Will save the `cache.json` file for next usage.
95impl 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        // if let Some(links) = self.cache.get(&number) {
107        //     return links.clone();
108        // }
109
110        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        // NOTE: should be a stream, but stream! can't be recursive...
245        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    /// Initialize a `RfcGraph` object and query the graph around the node `root`.
257    ///
258    /// The function will iterate in the graph recursively for `recursion_max`.
259    pub async fn get(root: i32, recursion_max: u32) -> Vec<i32> {
260        Self::default().rec_get_rfc(root, recursion_max).await
261    }
262}