Skip to main content

jsonette/
completion.rs

1/*
2 * Copyright (c) 2026 DevEtte.
3 *
4 * This project is dual-licensed under both the MIT License and the
5 * Apache License, Version 2.0 (the "License"). You may not use this
6 * file except in compliance with one of these licenses.
7 *
8 * You may obtain a copy of the Licenses at:
9 * - MIT: https://opensource.org
10 * - Apache 2.0: http://apache.org
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19//! JSONPath auto-completion and suggestion engine.
20
21use crate::json_node::JsonNode;
22use crate::types::CompletionItem;
23
24/// Provides autocomplete suggestions for a JSONPath prefix given the current document state.
25///
26/// # Arguments
27///
28/// * `node` - A reference to the parsed root `JsonNode` context.
29/// * `path_prefix` - The incomplete JSONPath prefix string slice typed by the user.
30///
31/// # Returns
32///
33/// A list of `CompletionItem` candidates suitable for autocomplete suggestions.
34pub fn completions_at(node: &JsonNode, path_prefix: &str) -> Vec<CompletionItem> {
35    let (parent_path, prefix) = match path_prefix.rfind('.') {
36        Some(idx) => {
37            let parent = &path_prefix[..idx];
38            let pref = &path_prefix[idx + 1..];
39            let parent = if parent.is_empty() { "$" } else { parent };
40            (parent, pref)
41        }
42        None => {
43            if path_prefix == "$" {
44                ("$", "")
45            } else {
46                return vec![];
47            }
48        }
49    };
50
51    let mut completions = Vec::new();
52
53    if let Ok(results) = crate::query::evaluate_path(node, parent_path) {
54        for result in results {
55            if let JsonNode::Object(pairs, _) = result {
56                for kv in pairs {
57                    if kv.key.starts_with(prefix) {
58                        let new_path = if parent_path == "$" {
59                            format!("$.{}", kv.key)
60                        } else {
61                            format!("{}.{}", parent_path, kv.key)
62                        };
63                        completions.push(CompletionItem {
64                            key: kv.key.clone(),
65                            path: new_path,
66                        });
67                    }
68                }
69            }
70        }
71    }
72
73    completions.sort_by(|a, b| a.key.cmp(&b.key));
74    completions.dedup_by(|a, b| a.key == b.key);
75
76    completions
77}
78
79#[cfg(test)]
80mod stub_tests {
81    use super::*;
82    use crate::parser::parse;
83
84    /// **Test Case**: Autocomplete at Root Object
85    ///
86    /// ### Description
87    /// Verifies that an empty JSONPath prefix `$.` returns all top-level keys.
88    ///
89    /// ### Test Procedure
90    /// 1. Parse a valid JSON object.
91    /// 2. Request completions at prefix `$.`.
92    ///
93    /// ### Expected Result
94    /// Returns all top-level keys sorted alphabetically.
95    #[test]
96    fn test_completions_at_root() {
97        let node = parse(r#"{"name": "Alice", "age": 30}"#).unwrap();
98        let comps = completions_at(&node, "$.");
99        assert_eq!(comps.len(), 2);
100        assert_eq!(comps[0].key, "age");
101        assert_eq!(comps[1].key, "name");
102    }
103
104    /// **Test Case**: Autocomplete with Partial Key Prefix
105    ///
106    /// ### Description
107    /// Verifies that typing a partial key `$.na` filters completions to only keys starting with `na`.
108    ///
109    /// ### Test Procedure
110    /// 1. Parse an object with multiple keys.
111    /// 2. Request completions at prefix `$.na`.
112    ///
113    /// ### Expected Result
114    /// Returns only keys matching the `na` prefix.
115    #[test]
116    fn test_completions_at_prefix() {
117        let node = parse(r#"{"name": "Alice", "age": 30, "nested": {"nav": 1}}"#).unwrap();
118        let comps = completions_at(&node, "$.na");
119        assert_eq!(comps.len(), 1);
120        assert_eq!(comps[0].key, "name");
121        assert_eq!(comps[0].path, "$.name");
122    }
123
124    /// **Test Case**: Autocomplete within Nested Object
125    ///
126    /// ### Description
127    /// Verifies that autocomplete successfully evaluates a nested JSONPath prefix
128    /// and correctly filters the nested object's keys.
129    ///
130    /// ### Test Procedure
131    /// 1. Parse an object with a nested object.
132    /// 2. Request completions at prefix `$.nested.na`.
133    ///
134    /// ### Expected Result
135    /// Returns keys inside `nested` that start with `na`, with the fully resolved paths.
136    #[test]
137    fn test_completions_nested() {
138        let node = parse(r#"{"nested": {"name": "Alice", "nav": 1}}"#).unwrap();
139        let comps = completions_at(&node, "$.nested.na");
140        assert_eq!(comps.len(), 2);
141        assert_eq!(comps[0].key, "name");
142        assert_eq!(comps[0].path, "$.nested.name");
143        assert_eq!(comps[1].key, "nav");
144        assert_eq!(comps[1].path, "$.nested.nav");
145    }
146}