ricecoder_external_lsp/mapping/
transformer.rs

1//! Output transformation engine
2//!
3//! Transforms LSP server responses to ricecoder models using configuration-driven rules.
4//! Supports JSON path expressions for field extraction and custom transformation functions.
5
6use crate::error::{ExternalLspError, Result};
7use crate::types::{CompletionMappingRules, DiagnosticsMappingRules, HoverMappingRules};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11use super::json_path::JsonPathParser;
12
13/// Transforms LSP server output to ricecoder models
14#[derive(Debug, Clone)]
15pub struct OutputTransformer {
16    /// Custom transformation functions (by name)
17    custom_transforms: HashMap<String, String>,
18}
19
20impl OutputTransformer {
21    /// Create a new output transformer
22    pub fn new() -> Self {
23        Self {
24            custom_transforms: HashMap::new(),
25        }
26    }
27
28    /// Create a transformer with custom transformation functions
29    pub fn with_transforms(custom_transforms: HashMap<String, String>) -> Self {
30        Self { custom_transforms }
31    }
32
33    /// Register a custom transformation function
34    pub fn register_transform(&mut self, name: String, function: String) {
35        self.custom_transforms.insert(name, function);
36    }
37
38    /// Transform a completion response using the provided rules
39    pub fn transform_completion(
40        &self,
41        response: &Value,
42        rules: &CompletionMappingRules,
43    ) -> Result<Vec<Value>> {
44        // Extract items array using JSON path
45        let items_parser = JsonPathParser::parse(&rules.items_path)?;
46        let items = items_parser.extract(response)?;
47
48        if items.is_empty() {
49            return Ok(Vec::new());
50        }
51
52        // If we got multiple items (from wildcard), use them; otherwise expect an array
53        let items_array = if items.len() == 1 && items[0].is_array() {
54            items[0].as_array().unwrap().clone()
55        } else if items.len() == 1 && items[0].is_object() {
56            // Single object, wrap in array
57            vec![items[0].clone()]
58        } else {
59            // Multiple items from wildcard
60            items
61        };
62
63        // Transform each item
64        let mut results = Vec::new();
65        for item in items_array {
66            let transformed = self.apply_field_mappings(&item, &rules.field_mappings)?;
67
68            // Apply custom transformation if specified
69            let final_item = if let Some(transform_name) = &rules.transform {
70                self.apply_custom_transform(&transformed, transform_name)?
71            } else {
72                transformed
73            };
74
75            results.push(final_item);
76        }
77
78        Ok(results)
79    }
80
81    /// Transform a diagnostics response using the provided rules
82    pub fn transform_diagnostics(
83        &self,
84        response: &Value,
85        rules: &DiagnosticsMappingRules,
86    ) -> Result<Vec<Value>> {
87        // Extract items array using JSON path
88        let items_parser = JsonPathParser::parse(&rules.items_path)?;
89        let items = items_parser.extract(response)?;
90
91        if items.is_empty() {
92            return Ok(Vec::new());
93        }
94
95        // If we got multiple items (from wildcard), use them; otherwise expect an array
96        let items_array = if items.len() == 1 && items[0].is_array() {
97            items[0].as_array().unwrap().clone()
98        } else if items.len() == 1 && items[0].is_object() {
99            // Single object, wrap in array
100            vec![items[0].clone()]
101        } else {
102            // Multiple items from wildcard
103            items
104        };
105
106        // Transform each item
107        let mut results = Vec::new();
108        for item in items_array {
109            let transformed = self.apply_field_mappings(&item, &rules.field_mappings)?;
110
111            // Apply custom transformation if specified
112            let final_item = if let Some(transform_name) = &rules.transform {
113                self.apply_custom_transform(&transformed, transform_name)?
114            } else {
115                transformed
116            };
117
118            results.push(final_item);
119        }
120
121        Ok(results)
122    }
123
124    /// Transform a hover response using the provided rules
125    pub fn transform_hover(
126        &self,
127        response: &Value,
128        rules: &HoverMappingRules,
129    ) -> Result<Value> {
130        // Extract content using JSON path
131        let content_parser = JsonPathParser::parse(&rules.content_path)?;
132        let content = content_parser.extract_single(response)?;
133
134        // Apply field mappings
135        let transformed = self.apply_field_mappings(&content, &rules.field_mappings)?;
136
137        // Apply custom transformation if specified
138        let final_value = if let Some(transform_name) = &rules.transform {
139            self.apply_custom_transform(&transformed, transform_name)?
140        } else {
141            transformed
142        };
143
144        Ok(final_value)
145    }
146
147    /// Apply field mappings to extract and rename fields
148    fn apply_field_mappings(
149        &self,
150        source: &Value,
151        field_mappings: &HashMap<String, String>,
152    ) -> Result<Value> {
153        let mut result = json!({});
154
155        for (target_field, source_path) in field_mappings {
156            let parser = JsonPathParser::parse(source_path)?;
157
158            match parser.extract_single(source) {
159                Ok(value) => {
160                    result[target_field] = value;
161                }
162                Err(_) => {
163                    // Field not found, skip it (optional field)
164                    continue;
165                }
166            }
167        }
168
169        Ok(result)
170    }
171
172    /// Apply a custom transformation function
173    fn apply_custom_transform(&self, value: &Value, transform_name: &str) -> Result<Value> {
174        // For now, we support built-in transformations
175        // Custom transformation functions would be evaluated here
176        match transform_name {
177            "identity" => Ok(value.clone()),
178            "stringify" => Ok(Value::String(value.to_string())),
179            _ => {
180                // Check if it's a registered custom transform
181                if self.custom_transforms.contains_key(transform_name) {
182                    // In a real implementation, we would evaluate the custom function
183                    // For now, just return the value as-is
184                    Ok(value.clone())
185                } else {
186                    Err(ExternalLspError::TransformationError(format!(
187                        "Unknown transformation function: {}",
188                        transform_name
189                    )))
190                }
191            }
192        }
193    }
194}
195
196impl Default for OutputTransformer {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_transform_completion_simple() {
208        let transformer = OutputTransformer::new();
209        let response = json!({
210            "result": {
211                "items": [
212                    {"label": "foo", "detail": "function"},
213                    {"label": "bar", "detail": "variable"}
214                ]
215            }
216        });
217
218        let mut field_mappings = HashMap::new();
219        field_mappings.insert("label".to_string(), "$.label".to_string());
220        field_mappings.insert("detail".to_string(), "$.detail".to_string());
221
222        let rules = CompletionMappingRules {
223            items_path: "$.result.items".to_string(),
224            field_mappings,
225            transform: None,
226        };
227
228        let results = transformer.transform_completion(&response, &rules).unwrap();
229        assert_eq!(results.len(), 2);
230        assert_eq!(results[0]["label"], "foo");
231        assert_eq!(results[1]["label"], "bar");
232    }
233
234    #[test]
235    fn test_transform_completion_with_wildcard() {
236        let transformer = OutputTransformer::new();
237        let response = json!({
238            "completions": [
239                {"name": "foo", "type": "function"},
240                {"name": "bar", "type": "variable"}
241            ]
242        });
243
244        let mut field_mappings = HashMap::new();
245        field_mappings.insert("label".to_string(), "$.name".to_string());
246        field_mappings.insert("kind".to_string(), "$.type".to_string());
247
248        let rules = CompletionMappingRules {
249            items_path: "$.completions[*]".to_string(),
250            field_mappings,
251            transform: None,
252        };
253
254        let results = transformer.transform_completion(&response, &rules).unwrap();
255        assert_eq!(results.len(), 2);
256        assert_eq!(results[0]["label"], "foo");
257        assert_eq!(results[0]["kind"], "function");
258    }
259
260    #[test]
261    fn test_transform_diagnostics() {
262        let transformer = OutputTransformer::new();
263        let response = json!({
264            "issues": [
265                {"message": "error", "line": 1},
266                {"message": "warning", "line": 2}
267            ]
268        });
269
270        let mut field_mappings = HashMap::new();
271        field_mappings.insert("message".to_string(), "$.message".to_string());
272        field_mappings.insert("line".to_string(), "$.line".to_string());
273
274        let rules = DiagnosticsMappingRules {
275            items_path: "$.issues".to_string(),
276            field_mappings,
277            transform: None,
278        };
279
280        let results = transformer.transform_diagnostics(&response, &rules).unwrap();
281        assert_eq!(results.len(), 2);
282        assert_eq!(results[0]["message"], "error");
283    }
284
285    #[test]
286    fn test_transform_hover() {
287        let transformer = OutputTransformer::new();
288        let response = json!({
289            "result": {
290                "contents": {
291                    "language": "rust",
292                    "value": "fn foo() -> i32"
293                }
294            }
295        });
296
297        let mut field_mappings = HashMap::new();
298        field_mappings.insert("language".to_string(), "$.language".to_string());
299        field_mappings.insert("value".to_string(), "$.value".to_string());
300
301        let rules = HoverMappingRules {
302            content_path: "$.result.contents".to_string(),
303            field_mappings,
304            transform: None,
305        };
306
307        let result = transformer.transform_hover(&response, &rules).unwrap();
308        assert_eq!(result["language"], "rust");
309        assert_eq!(result["value"], "fn foo() -> i32");
310    }
311
312    #[test]
313    fn test_missing_field_in_mapping() {
314        let transformer = OutputTransformer::new();
315        let response = json!({
316            "result": {
317                "items": [
318                    {"label": "foo"}
319                ]
320            }
321        });
322
323        let mut field_mappings = HashMap::new();
324        field_mappings.insert("label".to_string(), "$.label".to_string());
325        field_mappings.insert("detail".to_string(), "$.detail".to_string()); // Missing
326
327        let rules = CompletionMappingRules {
328            items_path: "$.result.items".to_string(),
329            field_mappings,
330            transform: None,
331        };
332
333        let results = transformer.transform_completion(&response, &rules).unwrap();
334        assert_eq!(results.len(), 1);
335        assert_eq!(results[0]["label"], "foo");
336        assert!(!results[0].get("detail").is_some() || results[0]["detail"].is_null());
337    }
338}