Skip to main content

json_structure/
json_source_locator.rs

1//! JSON source locator for tracking line/column positions.
2//!
3//! Provides utilities to map JSON Pointer paths to source locations
4//! in the original JSON text.
5
6use std::collections::HashMap;
7
8use crate::types::JsonLocation;
9
10/// Locates positions in JSON source text.
11///
12/// Maps JSON Pointer paths to their source locations (line/column)
13/// in the original JSON document.
14#[derive(Debug, Clone, Default)]
15pub struct JsonSourceLocator {
16    /// Map from JSON Pointer paths to source locations.
17    locations: HashMap<String, JsonLocation>,
18}
19
20impl JsonSourceLocator {
21    /// Creates a new source locator by parsing the given JSON text.
22    #[must_use]
23    pub fn new(json_text: &str) -> Self {
24        let mut locator = Self {
25            locations: HashMap::new(),
26        };
27        locator.parse(json_text);
28        locator
29    }
30
31    /// Parses JSON text and builds the location map.
32    fn parse(&mut self, text: &str) {
33        let mut line = 1usize;
34        let mut column = 1usize;
35        let mut path_stack: Vec<PathSegment> = Vec::new();
36        let chars: Vec<char> = text.chars().collect();
37        let len = chars.len();
38        let mut i = 0;
39
40        // Record root location
41        self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
42        if i < len {
43            self.locations.insert(String::new(), JsonLocation::new(line, column));
44        }
45
46        while i < len {
47            let ch = chars[i];
48
49            match ch {
50                '{' => {
51                    // Start of object
52                    i += 1;
53                    column += 1;
54                    self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
55                    
56                    // Parse object properties
57                    while i < len && chars[i] != '}' {
58                        self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
59                        if i >= len || chars[i] == '}' {
60                            break;
61                        }
62
63                        // Expect property name (string)
64                        if chars[i] == '\"' {
65                            let _key_line = line;
66                            let _key_column = column;
67                            let key = self.parse_string(&chars, &mut i, &mut line, &mut column);
68                            
69                            // Skip colon
70                            self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
71                            if i < len && chars[i] == ':' {
72                                i += 1;
73                                column += 1;
74                            }
75                            
76                            // Record value location
77                            self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
78                            path_stack.push(PathSegment::Property(key.clone()));
79                            let path = self.build_path(&path_stack);
80                            self.locations.insert(path, JsonLocation::new(line, column));
81                            
82                            // Skip value
83                            self.skip_value(&chars, &mut i, &mut line, &mut column, &mut path_stack);
84                            path_stack.pop();
85                            
86                            // Skip comma
87                            self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
88                            if i < len && chars[i] == ',' {
89                                i += 1;
90                                column += 1;
91                            }
92                        } else {
93                            // Invalid JSON, skip character
94                            i += 1;
95                            column += 1;
96                        }
97                    }
98                    
99                    // Skip closing brace
100                    if i < len && chars[i] == '}' {
101                        i += 1;
102                        column += 1;
103                    }
104                }
105                '[' => {
106                    // Start of array
107                    i += 1;
108                    column += 1;
109                    let mut index = 0usize;
110                    
111                    self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
112                    
113                    while i < len && chars[i] != ']' {
114                        self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
115                        if i >= len || chars[i] == ']' {
116                            break;
117                        }
118                        
119                        // Record element location
120                        path_stack.push(PathSegment::Index(index));
121                        let path = self.build_path(&path_stack);
122                        self.locations.insert(path, JsonLocation::new(line, column));
123                        
124                        // Skip value
125                        self.skip_value(&chars, &mut i, &mut line, &mut column, &mut path_stack);
126                        path_stack.pop();
127                        
128                        index += 1;
129                        
130                        // Skip comma
131                        self.skip_whitespace(&chars, &mut i, &mut line, &mut column);
132                        if i < len && chars[i] == ',' {
133                            i += 1;
134                            column += 1;
135                        }
136                    }
137                    
138                    // Skip closing bracket
139                    if i < len && chars[i] == ']' {
140                        i += 1;
141                        column += 1;
142                    }
143                }
144                '"' => {
145                    self.parse_string(&chars, &mut i, &mut line, &mut column);
146                }
147                '\n' => {
148                    i += 1;
149                    line += 1;
150                    column = 1;
151                }
152                _ => {
153                    // Skip other characters (numbers, booleans, null, whitespace)
154                    i += 1;
155                    column += 1;
156                }
157            }
158        }
159    }
160
161    /// Skips whitespace characters.
162    fn skip_whitespace(&self, chars: &[char], i: &mut usize, line: &mut usize, column: &mut usize) {
163        while *i < chars.len() {
164            match chars[*i] {
165                ' ' | '\t' | '\r' => {
166                    *i += 1;
167                    *column += 1;
168                }
169                '\n' => {
170                    *i += 1;
171                    *line += 1;
172                    *column = 1;
173                }
174                _ => break,
175            }
176        }
177    }
178
179    /// Parses a JSON string and returns its content.
180    fn parse_string(&self, chars: &[char], i: &mut usize, line: &mut usize, column: &mut usize) -> String {
181        let mut result = String::new();
182        
183        // Skip opening quote
184        if *i < chars.len() && chars[*i] == '"' {
185            *i += 1;
186            *column += 1;
187        }
188        
189        while *i < chars.len() {
190            let ch = chars[*i];
191            
192            if ch == '"' {
193                // End of string
194                *i += 1;
195                *column += 1;
196                break;
197            } else if ch == '\\' && *i + 1 < chars.len() {
198                // Escape sequence
199                *i += 1;
200                *column += 1;
201                let escaped = chars[*i];
202                *i += 1;
203                *column += 1;
204                
205                match escaped {
206                    'n' => result.push('\n'),
207                    'r' => result.push('\r'),
208                    't' => result.push('\t'),
209                    '\\' => result.push('\\'),
210                    '"' => result.push('"'),
211                    '/' => result.push('/'),
212                    'u' => {
213                        // Unicode escape - skip 4 hex digits
214                        for _ in 0..4 {
215                            if *i < chars.len() {
216                                *i += 1;
217                                *column += 1;
218                            }
219                        }
220                    }
221                    _ => result.push(escaped),
222                }
223            } else if ch == '\n' {
224                *i += 1;
225                *line += 1;
226                *column = 1;
227            } else {
228                result.push(ch);
229                *i += 1;
230                *column += 1;
231            }
232        }
233        
234        result
235    }
236
237    /// Skips a JSON value (recursively handles objects and arrays).
238    fn skip_value(
239        &mut self,
240        chars: &[char],
241        i: &mut usize,
242        line: &mut usize,
243        column: &mut usize,
244        path_stack: &mut Vec<PathSegment>,
245    ) {
246        self.skip_whitespace(chars, i, line, column);
247        
248        if *i >= chars.len() {
249            return;
250        }
251        
252        match chars[*i] {
253            '{' => {
254                *i += 1;
255                *column += 1;
256                self.skip_whitespace(chars, i, line, column);
257                
258                while *i < chars.len() && chars[*i] != '}' {
259                    self.skip_whitespace(chars, i, line, column);
260                    if *i >= chars.len() || chars[*i] == '}' {
261                        break;
262                    }
263                    
264                    // Parse property name
265                    if chars[*i] == '"' {
266                        let key = self.parse_string(chars, i, line, column);
267                        
268                        // Skip colon
269                        self.skip_whitespace(chars, i, line, column);
270                        if *i < chars.len() && chars[*i] == ':' {
271                            *i += 1;
272                            *column += 1;
273                        }
274                        
275                        // Record and skip value
276                        self.skip_whitespace(chars, i, line, column);
277                        path_stack.push(PathSegment::Property(key.clone()));
278                        let path = self.build_path(path_stack);
279                        self.locations.insert(path, JsonLocation::new(*line, *column));
280                        self.skip_value(chars, i, line, column, path_stack);
281                        path_stack.pop();
282                        
283                        // Skip comma
284                        self.skip_whitespace(chars, i, line, column);
285                        if *i < chars.len() && chars[*i] == ',' {
286                            *i += 1;
287                            *column += 1;
288                        }
289                    } else {
290                        *i += 1;
291                        *column += 1;
292                    }
293                }
294                
295                if *i < chars.len() && chars[*i] == '}' {
296                    *i += 1;
297                    *column += 1;
298                }
299            }
300            '[' => {
301                *i += 1;
302                *column += 1;
303                let mut index = 0usize;
304                
305                self.skip_whitespace(chars, i, line, column);
306                
307                while *i < chars.len() && chars[*i] != ']' {
308                    self.skip_whitespace(chars, i, line, column);
309                    if *i >= chars.len() || chars[*i] == ']' {
310                        break;
311                    }
312                    
313                    // Record and skip element
314                    path_stack.push(PathSegment::Index(index));
315                    let path = self.build_path(path_stack);
316                    self.locations.insert(path, JsonLocation::new(*line, *column));
317                    self.skip_value(chars, i, line, column, path_stack);
318                    path_stack.pop();
319                    
320                    index += 1;
321                    
322                    // Skip comma
323                    self.skip_whitespace(chars, i, line, column);
324                    if *i < chars.len() && chars[*i] == ',' {
325                        *i += 1;
326                        *column += 1;
327                    }
328                }
329                
330                if *i < chars.len() && chars[*i] == ']' {
331                    *i += 1;
332                    *column += 1;
333                }
334            }
335            '"' => {
336                self.parse_string(chars, i, line, column);
337            }
338            _ => {
339                // Number, boolean, null - skip until delimiter
340                while *i < chars.len() {
341                    let ch = chars[*i];
342                    if ch == ',' || ch == '}' || ch == ']' || ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
343                        break;
344                    }
345                    if ch == '\n' {
346                        *line += 1;
347                        *column = 1;
348                    } else {
349                        *column += 1;
350                    }
351                    *i += 1;
352                }
353            }
354        }
355    }
356
357    /// Builds a JSON Pointer path from the path stack.
358    fn build_path(&self, stack: &[PathSegment]) -> String {
359        if stack.is_empty() {
360            return String::new();
361        }
362        
363        let mut path = String::new();
364        for segment in stack {
365            path.push('/');
366            match segment {
367                PathSegment::Property(key) => {
368                    // Escape ~ and / in property names
369                    for ch in key.chars() {
370                        match ch {
371                            '~' => path.push_str("~0"),
372                            '/' => path.push_str("~1"),
373                            _ => path.push(ch),
374                        }
375                    }
376                }
377                PathSegment::Index(idx) => {
378                    path.push_str(&idx.to_string());
379                }
380            }
381        }
382        path
383    }
384
385    /// Gets the source location for a JSON Pointer path.
386    pub fn get_location(&self, path: impl AsRef<str>) -> JsonLocation {
387        self.locations
388            .get(path.as_ref())
389            .copied()
390            .unwrap_or_else(JsonLocation::unknown)
391    }
392
393    /// Returns true if the locator has a location for the given path.
394    pub fn has_location(&self, path: &str) -> bool {
395        self.locations.contains_key(path)
396    }
397}
398
399/// A segment in a JSON Pointer path.
400#[derive(Debug, Clone)]
401enum PathSegment {
402    Property(String),
403    Index(usize),
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_simple_object() {
412        let json = r#"{"name": "test", "value": 42}"#;
413        let locator = JsonSourceLocator::new(json);
414        
415        let root = locator.get_location("");
416        assert_eq!(root.line, 1);
417        assert_eq!(root.column, 1);
418        
419        let name = locator.get_location("/name");
420        assert!(!name.is_unknown());
421        
422        let value = locator.get_location("/value");
423        assert!(!value.is_unknown());
424    }
425
426    #[test]
427    fn test_nested_object() {
428        let json = r#"{
429  "outer": {
430    "inner": "value"
431  }
432}"#;
433        let locator = JsonSourceLocator::new(json);
434        
435        let inner = locator.get_location("/outer/inner");
436        assert!(!inner.is_unknown());
437        assert_eq!(inner.line, 3);
438    }
439
440    #[test]
441    fn test_array() {
442        let json = r#"[1, 2, 3]"#;
443        let locator = JsonSourceLocator::new(json);
444        
445        let first = locator.get_location("/0");
446        assert!(!first.is_unknown());
447        
448        let second = locator.get_location("/1");
449        assert!(!second.is_unknown());
450    }
451
452    #[test]
453    fn test_unknown_path() {
454        let json = r#"{"name": "test"}"#;
455        let locator = JsonSourceLocator::new(json);
456        
457        let unknown = locator.get_location("/nonexistent");
458        assert!(unknown.is_unknown());
459    }
460}