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}