1use serde::{Deserialize, Serialize};
2use urlencoding::encode;
3
4use crate::api::DATATRACKER_BASE_URL;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum DocumentType {
9 Rfc(u32),
11 Draft(String),
13}
14
15impl DocumentType {
16 pub fn from_user_input(s: &str) -> Self {
24 let s = s.trim().to_lowercase();
25
26 if let Some(num_str) = s.strip_prefix("rfc") {
27 if let Ok(num) = num_str.trim().parse::<u32>() {
28 return DocumentType::Rfc(num);
29 }
30 }
31
32 if let Ok(num) = s.parse::<u32>() {
33 return DocumentType::Rfc(num);
34 }
35
36 if s.starts_with("draft-") {
37 DocumentType::Draft(s)
38 } else {
39 DocumentType::Draft(format!("draft-{}", s))
40 }
41 }
42
43 pub fn from_canonical_name(name: &str) -> Self {
48 if let Some(num_str) = name.strip_prefix("rfc") {
49 if let Ok(num) = num_str.parse::<u32>() {
50 return DocumentType::Rfc(num);
51 }
52 }
53 DocumentType::Draft(name.to_string())
54 }
55
56 pub fn name(&self) -> String {
58 match self {
59 DocumentType::Rfc(num) => format!("rfc{}", num),
60 DocumentType::Draft(name) => name.clone(),
61 }
62 }
63
64 pub fn display_name(&self) -> String {
66 match self {
67 DocumentType::Rfc(num) => format!("RFC {}", num),
68 DocumentType::Draft(name) => name.clone(),
69 }
70 }
71
72 pub fn datatracker_url(&self) -> String {
74 match self {
75 DocumentType::Rfc(num) => format!("{}/doc/rfc{}/", DATATRACKER_BASE_URL, num),
76 DocumentType::Draft(name) => {
77 format!("{}/doc/{}/", DATATRACKER_BASE_URL, encode(name))
78 }
79 }
80 }
81}
82
83impl std::fmt::Display for DocumentType {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 write!(f, "{}", self.display_name())
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91pub enum Format {
92 Html,
93 Text,
94}
95
96impl Format {
97 pub fn extension(&self) -> &'static str {
98 match self {
99 Format::Html => "html",
100 Format::Text => "txt",
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Document {
112 pub name: String,
114 pub title: String,
116 pub doc_type: DocumentType,
117}
118
119impl Document {
120 pub fn new(name: String, title: String, doc_type: DocumentType) -> Self {
121 Self {
122 name,
123 title,
124 doc_type,
125 }
126 }
127
128 pub fn short_title(&self, max_len: usize) -> String {
130 if self.title.chars().count() <= max_len {
131 self.title.clone()
132 } else {
133 let truncated: String = self.title.chars().take(max_len.saturating_sub(3)).collect();
134 format!("{}...", truncated)
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_from_user_input_rfc() {
145 assert_eq!(
146 DocumentType::from_user_input("9000"),
147 DocumentType::Rfc(9000)
148 );
149 assert_eq!(
150 DocumentType::from_user_input("rfc9000"),
151 DocumentType::Rfc(9000)
152 );
153 assert_eq!(
154 DocumentType::from_user_input("RFC9000"),
155 DocumentType::Rfc(9000)
156 );
157 assert_eq!(
158 DocumentType::from_user_input("RFC 9000"),
159 DocumentType::Rfc(9000)
160 );
161 assert_eq!(
162 DocumentType::from_user_input(" rfc9000 "),
163 DocumentType::Rfc(9000)
164 );
165 }
166
167 #[test]
168 fn test_from_user_input_draft() {
169 assert_eq!(
170 DocumentType::from_user_input("draft-ietf-quic-transport-34"),
171 DocumentType::Draft("draft-ietf-quic-transport-34".to_string())
172 );
173 assert_eq!(
175 DocumentType::from_user_input("ietf-quic-transport"),
176 DocumentType::Draft("draft-ietf-quic-transport".to_string())
177 );
178 }
179
180 #[test]
181 fn test_from_canonical_name() {
182 assert_eq!(
183 DocumentType::from_canonical_name("rfc9000"),
184 DocumentType::Rfc(9000)
185 );
186 assert_eq!(
187 DocumentType::from_canonical_name("draft-ietf-quic-transport-34"),
188 DocumentType::Draft("draft-ietf-quic-transport-34".to_string())
189 );
190 assert_eq!(
192 DocumentType::from_canonical_name("rfcfoo"),
193 DocumentType::Draft("rfcfoo".to_string())
194 );
195 }
196
197 #[test]
198 fn test_document_type_display() {
199 assert_eq!(DocumentType::Rfc(9000).to_string(), "RFC 9000");
200 assert_eq!(
201 DocumentType::Draft("draft-ietf-quic-transport".to_string()).to_string(),
202 "draft-ietf-quic-transport"
203 );
204 }
205
206 #[test]
207 fn test_datatracker_url() {
208 assert_eq!(
209 DocumentType::Rfc(9000).datatracker_url(),
210 "https://datatracker.ietf.org/doc/rfc9000/"
211 );
212 assert_eq!(
213 DocumentType::Draft("draft-ietf-quic-transport".to_string()).datatracker_url(),
214 "https://datatracker.ietf.org/doc/draft-ietf-quic-transport/"
215 );
216 assert_eq!(
217 DocumentType::Draft("draft with spaces/and/slashes".to_string()).datatracker_url(),
218 "https://datatracker.ietf.org/doc/draft%20with%20spaces%2Fand%2Fslashes/"
219 );
220 }
221
222 #[test]
223 fn test_short_title() {
224 let doc = Document::new(
225 "rfc9000".to_string(),
226 "A Very Long Title That Needs Truncation".to_string(),
227 DocumentType::Rfc(9000),
228 );
229
230 assert_eq!(
232 doc.short_title(100),
233 "A Very Long Title That Needs Truncation"
234 );
235
236 assert_eq!(doc.short_title(20), "A Very Long Title...");
238
239 assert_eq!(doc.short_title(3), "...");
241 assert_eq!(doc.short_title(0), "...");
242 }
243
244 #[test]
245 fn test_short_title_utf8() {
246 let doc = Document::new(
248 "rfc1234".to_string(),
249 "Café résumé naïve".to_string(),
250 DocumentType::Rfc(1234),
251 );
252
253 let result = doc.short_title(10);
255 assert!(result.ends_with("..."));
256 assert!(result.chars().count() <= 10);
257 }
258}