1use crate::domains::{
2 CrossReference, DomainObject, DomainValidator, ReferenceType, ReferenceValidationResult,
3};
4use crate::error::BuildError;
5use std::collections::HashMap;
9
10pub struct RstDomain {
12 documents: HashMap<String, DomainObject>,
14 sections: HashMap<String, DomainObject>,
16}
17
18impl Default for RstDomain {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl RstDomain {
25 pub fn new() -> Self {
27 Self {
28 documents: HashMap::new(),
29 sections: HashMap::new(),
30 }
31 }
32
33 pub fn register_document(
35 &mut self,
36 docname: String,
37 title: String,
38 location: crate::domains::ReferenceLocation,
39 ) -> Result<(), BuildError> {
40 let object = DomainObject {
41 id: format!("doc:{}", docname),
42 name: docname.clone(),
43 object_type: "document".to_string(),
44 domain: "rst".to_string(),
45 definition_location: location,
46 qualified_name: docname.clone(),
47 metadata: {
48 let mut meta = HashMap::new();
49 meta.insert("title".to_string(), title);
50 meta
51 },
52 signature: None,
53 docstring: None,
54 };
55
56 self.documents.insert(docname, object);
57 Ok(())
58 }
59
60 pub fn register_section(
62 &mut self,
63 label: String,
64 title: String,
65 docname: String,
66 location: crate::domains::ReferenceLocation,
67 ) -> Result<(), BuildError> {
68 let qualified_name = format!("{}#{}", docname, label);
69 let object = DomainObject {
70 id: format!("ref:{}", label),
71 name: label.clone(),
72 object_type: "section".to_string(),
73 domain: "rst".to_string(),
74 definition_location: location,
75 qualified_name: qualified_name.clone(),
76 metadata: {
77 let mut meta = HashMap::new();
78 meta.insert("title".to_string(), title);
79 meta.insert("docname".to_string(), docname);
80 meta
81 },
82 signature: None,
83 docstring: None,
84 };
85
86 self.sections.insert(label, object);
87 Ok(())
88 }
89
90 pub fn register_label(
92 &mut self,
93 label: String,
94 label_type: String, title: Option<String>,
96 docname: String,
97 location: crate::domains::ReferenceLocation,
98 ) -> Result<(), BuildError> {
99 let qualified_name = format!("{}#{}", docname, label);
100 let object = DomainObject {
101 id: format!("{}:{}", label_type, label),
102 name: label.clone(),
103 object_type: label_type,
104 domain: "rst".to_string(),
105 definition_location: location,
106 qualified_name: qualified_name.clone(),
107 metadata: {
108 let mut meta = HashMap::new();
109 if let Some(title) = title {
110 meta.insert("title".to_string(), title);
111 }
112 meta.insert("docname".to_string(), docname);
113 meta
114 },
115 signature: None,
116 docstring: None,
117 };
118
119 self.sections.insert(label, object);
120 Ok(())
121 }
122
123 fn find_suggestions(&self, target: &str, ref_type: &ReferenceType) -> Vec<String> {
125 let mut suggestions = Vec::new();
126
127 let objects = match ref_type {
128 ReferenceType::Document => &self.documents,
129 ReferenceType::Section => &self.sections,
130 _ => &self.sections, };
132
133 for (key, obj) in objects {
135 if key.contains(target) || target.contains(key) {
137 suggestions.push(key.clone());
138 }
139 if let Some(title) = obj.metadata.get("title") {
141 if title.to_lowercase().contains(&target.to_lowercase()) {
142 suggestions.push(key.clone());
143 }
144 }
145 }
146
147 suggestions.sort();
148 suggestions.dedup();
149 suggestions.truncate(5); suggestions
152 }
153}
154
155impl DomainValidator for RstDomain {
156 fn domain_name(&self) -> &str {
157 "rst"
158 }
159
160 fn supported_reference_types(&self) -> Vec<ReferenceType> {
161 vec![
162 ReferenceType::Document,
163 ReferenceType::Section,
164 ReferenceType::Custom("numref".to_string()),
165 ]
166 }
167
168 fn register_object(&mut self, object: DomainObject) -> Result<(), BuildError> {
169 match object.object_type.as_str() {
170 "document" => {
171 self.documents.insert(object.name.clone(), object);
172 }
173 "section" | "figure" | "table" | "code-block" => {
174 self.sections.insert(object.name.clone(), object);
175 }
176 _ => {
177 return Err(BuildError::ValidationError(format!(
178 "Unknown RST object type: {}",
179 object.object_type
180 )));
181 }
182 }
183 Ok(())
184 }
185
186 fn resolve_reference(&self, reference: &CrossReference) -> Option<DomainObject> {
187 match reference.ref_type {
188 ReferenceType::Document => {
189 let target = reference.target.trim_end_matches(".rst");
191 self.documents
192 .get(target)
193 .cloned()
194 .or_else(|| self.documents.get(&reference.target).cloned())
195 }
196 ReferenceType::Section => self.sections.get(&reference.target).cloned(),
197 _ => {
198 self.sections
200 .get(&reference.target)
201 .cloned()
202 .or_else(|| self.documents.get(&reference.target).cloned())
203 }
204 }
205 }
206
207 fn validate_reference(&self, reference: &CrossReference) -> ReferenceValidationResult {
208 if let Some(target_object) = self.resolve_reference(reference) {
209 ReferenceValidationResult {
210 reference: reference.clone(),
211 is_valid: true,
212 target_object: Some(target_object),
213 error_message: None,
214 suggestions: Vec::new(),
215 }
216 } else {
217 let suggestions = self.find_suggestions(&reference.target, &reference.ref_type);
218 let error_message = match reference.ref_type {
219 ReferenceType::Document => {
220 if suggestions.is_empty() {
221 format!("Document '{}' not found", reference.target)
222 } else {
223 format!(
224 "Document '{}' not found. Did you mean: {}?",
225 reference.target,
226 suggestions.join(", ")
227 )
228 }
229 }
230 ReferenceType::Section => {
231 if suggestions.is_empty() {
232 format!("Section label '{}' not found", reference.target)
233 } else {
234 format!(
235 "Section label '{}' not found. Did you mean: {}?",
236 reference.target,
237 suggestions.join(", ")
238 )
239 }
240 }
241 _ => {
242 if suggestions.is_empty() {
243 format!("Reference target '{}' not found", reference.target)
244 } else {
245 format!(
246 "Reference target '{}' not found. Did you mean: {}?",
247 reference.target,
248 suggestions.join(", ")
249 )
250 }
251 }
252 };
253
254 ReferenceValidationResult {
255 reference: reference.clone(),
256 is_valid: false,
257 target_object: None,
258 error_message: Some(error_message),
259 suggestions,
260 }
261 }
262 }
263
264 fn get_all_objects(&self) -> Vec<&DomainObject> {
265 let mut objects = Vec::new();
266 objects.extend(self.documents.values());
267 objects.extend(self.sections.values());
268 objects
269 }
270
271 fn clear_objects(&mut self) {
272 self.documents.clear();
273 self.sections.clear();
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::domains::ReferenceLocation;
281
282 fn create_test_location() -> ReferenceLocation {
283 ReferenceLocation {
284 docname: "test.rst".to_string(),
285 lineno: Some(10),
286 column: Some(5),
287 source_path: Some("test.rst".to_string()),
288 }
289 }
290
291 #[test]
292 fn test_rst_domain_creation() {
293 let domain = RstDomain::new();
294 assert_eq!(domain.domain_name(), "rst");
295 assert!(domain
296 .supported_reference_types()
297 .contains(&ReferenceType::Document));
298 assert!(domain
299 .supported_reference_types()
300 .contains(&ReferenceType::Section));
301 }
302
303 #[test]
304 fn test_register_document() {
305 let mut domain = RstDomain::new();
306
307 let result = domain.register_document(
308 "index".to_string(),
309 "Home Page".to_string(),
310 create_test_location(),
311 );
312
313 assert!(result.is_ok());
314 assert_eq!(domain.documents.len(), 1);
315
316 let doc = domain.documents.get("index").unwrap();
317 assert_eq!(doc.name, "index");
318 assert_eq!(doc.object_type, "document");
319 assert_eq!(doc.metadata.get("title"), Some(&"Home Page".to_string()));
320 }
321
322 #[test]
323 fn test_register_section() {
324 let mut domain = RstDomain::new();
325
326 let result = domain.register_section(
327 "introduction".to_string(),
328 "Introduction".to_string(),
329 "index".to_string(),
330 create_test_location(),
331 );
332
333 assert!(result.is_ok());
334 assert_eq!(domain.sections.len(), 1);
335
336 let section = domain.sections.get("introduction").unwrap();
337 assert_eq!(section.name, "introduction");
338 assert_eq!(section.object_type, "section");
339 assert_eq!(
340 section.metadata.get("title"),
341 Some(&"Introduction".to_string())
342 );
343 assert_eq!(section.metadata.get("docname"), Some(&"index".to_string()));
344 }
345
346 #[test]
347 fn test_document_reference_validation() {
348 let mut domain = RstDomain::new();
349
350 domain
351 .register_document(
352 "getting-started".to_string(),
353 "Getting Started".to_string(),
354 create_test_location(),
355 )
356 .unwrap();
357
358 let valid_ref = CrossReference {
360 ref_type: ReferenceType::Document,
361 target: "getting-started".to_string(),
362 display_text: None,
363 source_location: create_test_location(),
364 is_external: false,
365 };
366
367 let result = domain.validate_reference(&valid_ref);
368 assert!(result.is_valid);
369 assert!(result.target_object.is_some());
370
371 let valid_ref_ext = CrossReference {
373 ref_type: ReferenceType::Document,
374 target: "getting-started.rst".to_string(),
375 display_text: None,
376 source_location: create_test_location(),
377 is_external: false,
378 };
379
380 let result = domain.validate_reference(&valid_ref_ext);
381 assert!(result.is_valid);
382
383 let invalid_ref = CrossReference {
385 ref_type: ReferenceType::Document,
386 target: "nonexistent".to_string(),
387 display_text: None,
388 source_location: create_test_location(),
389 is_external: false,
390 };
391
392 let result = domain.validate_reference(&invalid_ref);
393 assert!(!result.is_valid);
394 assert!(result.error_message.is_some());
395 }
396
397 #[test]
398 fn test_section_reference_validation() {
399 let mut domain = RstDomain::new();
400
401 domain
402 .register_section(
403 "api-reference".to_string(),
404 "API Reference".to_string(),
405 "api".to_string(),
406 create_test_location(),
407 )
408 .unwrap();
409
410 let valid_ref = CrossReference {
412 ref_type: ReferenceType::Section,
413 target: "api-reference".to_string(),
414 display_text: None,
415 source_location: create_test_location(),
416 is_external: false,
417 };
418
419 let result = domain.validate_reference(&valid_ref);
420 assert!(result.is_valid);
421 assert!(result.target_object.is_some());
422
423 let invalid_ref = CrossReference {
425 ref_type: ReferenceType::Section,
426 target: "api".to_string(), display_text: None,
428 source_location: create_test_location(),
429 is_external: false,
430 };
431
432 let result = domain.validate_reference(&invalid_ref);
433 assert!(!result.is_valid);
434 assert!(!result.suggestions.is_empty());
435 }
436}