1use anyhow::{Context, Result};
2use reqwest::Client;
3use serde::Deserialize;
4
5use crate::models::{DocumentType, Format};
6
7#[derive(Debug, Deserialize)]
8struct DraftInfo {
9 rev: Option<String>,
10}
11
12pub struct DocumentFetcher {
18 client: Client,
19}
20
21impl DocumentFetcher {
22 pub fn new() -> Result<Self> {
24 Ok(Self::with_client(super::build_http_client()?))
25 }
26
27 pub fn with_client(client: Client) -> Self {
31 Self { client }
32 }
33
34 pub async fn fetch(&self, doc: &DocumentType) -> Result<(String, Format)> {
39 let doc = self.resolve_draft_version(doc).await?;
40
41 let text_url = self.text_url(&doc);
42 match self.fetch_content(&text_url).await {
43 Ok(content) => Ok((content, Format::Text)),
44 Err(text_err) => {
45 let html_url = self.html_url(&doc);
46 let content = self.fetch_content(&html_url).await.with_context(|| {
47 format!(
48 "Plain text fetch failed ({}); HTML fallback also failed",
49 text_err
50 )
51 })?;
52 Ok((content, Format::Html))
53 }
54 }
55 }
56
57 async fn resolve_draft_version(&self, doc: &DocumentType) -> Result<DocumentType> {
60 match doc {
61 DocumentType::Rfc(_) => Ok(doc.clone()),
62 DocumentType::Draft(name) => {
63 if Self::has_version_suffix(name) {
64 return Ok(doc.clone());
65 }
66
67 let url = format!("https://datatracker.ietf.org/doc/{}/doc.json", name);
68 let response = self
69 .client
70 .get(&url)
71 .send()
72 .await
73 .context("Failed to query draft info")?;
74
75 if !response.status().is_success() {
76 anyhow::bail!("Draft not found: {}", name);
77 }
78
79 let info: DraftInfo = response
80 .json()
81 .await
82 .context("Failed to parse draft info")?;
83
84 match info.rev {
85 Some(rev) => Ok(DocumentType::Draft(format!("{}-{}", name, rev))),
86 None => Ok(doc.clone()),
87 }
88 }
89 }
90 }
91
92 fn has_version_suffix(name: &str) -> bool {
96 if let Some(last_dash) = name.rfind('-') {
97 let suffix = &name[last_dash + 1..];
98 !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit())
99 } else {
100 false
101 }
102 }
103
104 pub fn html_url(&self, doc: &DocumentType) -> String {
106 match doc {
107 DocumentType::Rfc(num) => {
108 format!("https://www.rfc-editor.org/rfc/rfc{}.html", num)
109 }
110 DocumentType::Draft(name) => {
111 format!("https://datatracker.ietf.org/doc/html/{}", name)
112 }
113 }
114 }
115
116 pub fn text_url(&self, doc: &DocumentType) -> String {
118 match doc {
119 DocumentType::Rfc(num) => {
120 format!("https://www.rfc-editor.org/rfc/rfc{}.txt", num)
121 }
122 DocumentType::Draft(name) => {
123 format!("https://www.ietf.org/archive/id/{}.txt", name)
124 }
125 }
126 }
127
128 async fn fetch_content(&self, url: &str) -> Result<String> {
129 let response = self
130 .client
131 .get(url)
132 .send()
133 .await
134 .context("Failed to fetch document")?;
135
136 if !response.status().is_success() {
137 anyhow::bail!("Failed to fetch {}: HTTP {}", url, response.status());
138 }
139
140 response
141 .text()
142 .await
143 .context("Failed to read document content")
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_rfc_urls() {
153 let editor = DocumentFetcher::new().unwrap();
154
155 assert_eq!(
156 editor.html_url(&DocumentType::Rfc(9000)),
157 "https://www.rfc-editor.org/rfc/rfc9000.html"
158 );
159 assert_eq!(
160 editor.text_url(&DocumentType::Rfc(9000)),
161 "https://www.rfc-editor.org/rfc/rfc9000.txt"
162 );
163 }
164
165 #[test]
166 fn test_draft_urls() {
167 let editor = DocumentFetcher::new().unwrap();
168 let draft = DocumentType::Draft("draft-ietf-quic-transport-34".to_string());
169
170 assert_eq!(
171 editor.html_url(&draft),
172 "https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-34"
173 );
174 assert_eq!(
175 editor.text_url(&draft),
176 "https://www.ietf.org/archive/id/draft-ietf-quic-transport-34.txt"
177 );
178 }
179
180 #[test]
181 fn test_has_version_suffix() {
182 assert!(DocumentFetcher::has_version_suffix(
184 "draft-ietf-quic-transport-34"
185 ));
186 assert!(DocumentFetcher::has_version_suffix("draft-foo-00"));
187 assert!(DocumentFetcher::has_version_suffix("draft-test-123456"));
188
189 assert!(!DocumentFetcher::has_version_suffix(
191 "draft-ietf-quic-transport"
192 ));
193 assert!(!DocumentFetcher::has_version_suffix("draft-foo-bar-v2")); assert!(!DocumentFetcher::has_version_suffix("draft-foo-bar-")); assert!(!DocumentFetcher::has_version_suffix("draftname")); assert!(!DocumentFetcher::has_version_suffix("")); }
198}