ricecoder_external_lsp/mapping/
diagnostics.rs

1//! Diagnostics output mapping
2//!
3//! Maps LSP diagnostic responses to ricecoder Diagnostic models.
4//! Supports custom field mappings via configuration and transformation functions.
5
6use crate::error::Result;
7use crate::types::DiagnosticsMappingRules;
8use serde_json::Value;
9
10use super::transformer::OutputTransformer;
11
12/// Maps LSP diagnostic responses to ricecoder models
13#[derive(Debug, Clone)]
14pub struct DiagnosticsMapper {
15    transformer: OutputTransformer,
16}
17
18impl DiagnosticsMapper {
19    /// Create a new diagnostics mapper
20    pub fn new() -> Self {
21        Self {
22            transformer: OutputTransformer::new(),
23        }
24    }
25
26    /// Create a mapper with custom transformations
27    pub fn with_transformer(transformer: OutputTransformer) -> Self {
28        Self { transformer }
29    }
30
31    /// Map an LSP diagnostics response to ricecoder models
32    ///
33    /// # Arguments
34    ///
35    /// * `response` - The LSP server response (typically from textDocument/publishDiagnostics)
36    /// * `rules` - The mapping rules from configuration
37    ///
38    /// # Returns
39    ///
40    /// A vector of mapped diagnostic items
41    pub fn map(&self, response: &Value, rules: &DiagnosticsMappingRules) -> Result<Vec<Value>> {
42        self.transformer.transform_diagnostics(response, rules)
43    }
44
45    /// Map a single diagnostic item
46    ///
47    /// This is useful for mapping individual items when the response structure
48    /// doesn't match the expected array format.
49    pub fn map_item(&self, item: &Value, rules: &DiagnosticsMappingRules) -> Result<Value> {
50        // Create a wrapper response with the item
51        let wrapped = serde_json::json!({
52            "result": {
53                "items": [item]
54            }
55        });
56
57        // Use default rules that expect this structure
58        let default_rules = DiagnosticsMappingRules {
59            items_path: "$.result.items".to_string(),
60            field_mappings: rules.field_mappings.clone(),
61            transform: rules.transform.clone(),
62        };
63
64        let results = self.transformer.transform_diagnostics(&wrapped, &default_rules)?;
65
66        if results.is_empty() {
67            return Err(crate::error::ExternalLspError::TransformationError(
68                "Failed to map diagnostic item".to_string(),
69            ));
70        }
71
72        Ok(results[0].clone())
73    }
74}
75
76impl Default for DiagnosticsMapper {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::collections::HashMap;
86
87    #[test]
88    fn test_map_diagnostics_response() {
89        let mapper = DiagnosticsMapper::new();
90        let response = serde_json::json!({
91            "result": [
92                {
93                    "message": "error: undefined variable",
94                    "range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 5}},
95                    "severity": 1
96                },
97                {
98                    "message": "warning: unused variable",
99                    "range": {"start": {"line": 2, "character": 0}, "end": {"line": 2, "character": 3}},
100                    "severity": 2
101                }
102            ]
103        });
104
105        let mut field_mappings = HashMap::new();
106        field_mappings.insert("message".to_string(), "$.message".to_string());
107        field_mappings.insert("range".to_string(), "$.range".to_string());
108        field_mappings.insert("severity".to_string(), "$.severity".to_string());
109
110        let rules = DiagnosticsMappingRules {
111            items_path: "$.result".to_string(),
112            field_mappings,
113            transform: None,
114        };
115
116        let results = mapper.map(&response, &rules).unwrap();
117        assert_eq!(results.len(), 2);
118        assert_eq!(results[0]["message"], "error: undefined variable");
119        assert_eq!(results[1]["message"], "warning: unused variable");
120    }
121
122    #[test]
123    fn test_map_diagnostics_with_custom_structure() {
124        let mapper = DiagnosticsMapper::new();
125        let response = serde_json::json!({
126            "issues": [
127                {"error_message": "error", "error_line": 1},
128                {"error_message": "warning", "error_line": 2}
129            ]
130        });
131
132        let mut field_mappings = HashMap::new();
133        field_mappings.insert("message".to_string(), "$.error_message".to_string());
134        field_mappings.insert("line".to_string(), "$.error_line".to_string());
135
136        let rules = DiagnosticsMappingRules {
137            items_path: "$.issues".to_string(),
138            field_mappings,
139            transform: None,
140        };
141
142        let results = mapper.map(&response, &rules).unwrap();
143        assert_eq!(results.len(), 2);
144        assert_eq!(results[0]["message"], "error");
145        assert_eq!(results[0]["line"], 1);
146    }
147
148    #[test]
149    fn test_map_single_diagnostic() {
150        let mapper = DiagnosticsMapper::new();
151        let item = serde_json::json!({
152            "message": "test error",
153            "severity": 1,
154            "range": {"start": {"line": 0, "character": 0}}
155        });
156
157        let mut field_mappings = HashMap::new();
158        field_mappings.insert("message".to_string(), "$.message".to_string());
159        field_mappings.insert("severity".to_string(), "$.severity".to_string());
160
161        let rules = DiagnosticsMappingRules {
162            items_path: "$.result.items".to_string(),
163            field_mappings,
164            transform: None,
165        };
166
167        let result = mapper.map_item(&item, &rules).unwrap();
168        assert_eq!(result["message"], "test error");
169        assert_eq!(result["severity"], 1);
170    }
171
172    #[test]
173    fn test_map_empty_diagnostics() {
174        let mapper = DiagnosticsMapper::new();
175        let response = serde_json::json!({
176            "result": []
177        });
178
179        let field_mappings = HashMap::new();
180        let rules = DiagnosticsMappingRules {
181            items_path: "$.result".to_string(),
182            field_mappings,
183            transform: None,
184        };
185
186        let results = mapper.map(&response, &rules).unwrap();
187        assert_eq!(results.len(), 0);
188    }
189}