json_eval_rs/jsoneval/
path_utils.rs1use serde_json::Value;
2use std::borrow::Cow;
3
4#[inline]
14pub fn normalize_to_json_pointer(path: &str) -> Cow<'_, str> {
15 if path.is_empty() {
16 return Cow::Borrowed("");
17 }
18
19 if path.starts_with("#/") {
20 let stripped = &path[1..];
21 if !stripped.contains("//") {
22 return Cow::Borrowed(stripped);
23 }
24 }
25
26 if path.starts_with('/') && !path.contains("//") {
27 return if path == "/" {
28 Cow::Borrowed("")
29 } else {
30 Cow::Borrowed(path)
31 };
32 }
33
34 let mut normalized = String::with_capacity(path.len() + 1);
35 let source = if path.starts_with("#/") {
36 &path[1..]
37 } else if !path.starts_with('/') {
38 normalized.push('/');
39 path
40 } else {
41 path
42 };
43
44 let mut prev_slash = normalized.ends_with('/');
45 for ch in source.chars() {
46 let c = if ch == '.' && !path.starts_with('/') && !path.starts_with('#') {
47 '/'
48 } else {
49 ch
50 };
51 if c == '/' {
52 if !prev_slash {
53 normalized.push('/');
54 }
55 prev_slash = true;
56 } else {
57 normalized.push(c);
58 prev_slash = false;
59 }
60 }
61
62 if normalized == "/" {
63 Cow::Borrowed("")
64 } else {
65 Cow::Owned(normalized)
66 }
67}
68
69#[inline]
73pub fn schema_path_to_data_pointer(path: &str) -> Cow<'_, str> {
74 if path.is_empty() {
75 return Cow::Borrowed("");
76 }
77
78 let no_hash = if path.starts_with('#') {
79 &path[1..]
80 } else {
81 path
82 };
83
84 let clean_path = if no_hash.starts_with('/') {
85 &no_hash[1..]
86 } else {
87 no_hash
88 };
89
90 if !clean_path.contains("properties/") && clean_path != "properties" {
91 if path.starts_with('/') && path.len() == clean_path.len() + 1 {
92 return Cow::Borrowed(path);
93 }
94 let mut s = String::with_capacity(clean_path.len() + 1);
95 s.push('/');
96 s.push_str(clean_path);
97 return Cow::Owned(s);
98 }
99
100 let parts = clean_path.split('/');
101 let mut s = String::with_capacity(clean_path.len() + 1);
102 for part in parts {
103 if part.is_empty() || part == "properties" {
104 continue;
105 }
106 s.push('/');
107 s.push_str(part);
108 }
109
110 if s.is_empty() {
111 Cow::Borrowed("")
112 } else {
113 Cow::Owned(s)
114 }
115}
116
117#[inline]
126pub fn dot_notation_to_schema_pointer(path: &str) -> String {
127 if path.starts_with('#') || path.starts_with('/') {
129 return path.to_string();
130 }
131
132 if path.starts_with("properties.") || path.contains(".properties.") {
134 return format!("#/{}", path.replace('.', "/"));
135 }
136
137 let parts: Vec<&str> = path.split('.').collect();
139 if parts.is_empty() {
140 return "#/".to_string();
141 }
142
143 let mut result = String::from("#");
147 for (i, part) in parts.iter().enumerate() {
148 if part.eq(&"properties") {
149 continue;
150 }
151
152 if i > 0 && !path.starts_with('$') {
153 result.push_str("/properties");
154 }
155 result.push_str("/");
156 result.push_str(part);
157 }
158
159 result
160}
161
162#[inline]
171pub fn pointer_to_dot_notation(path: &str) -> String {
172 if path.is_empty() {
173 return String::new();
174 }
175
176 if !path.starts_with('#') && !path.starts_with('/') {
178 return path.to_string();
179 }
180
181 let clean_path = if path.starts_with("#/") {
183 &path[2..]
184 } else if path.starts_with('/') {
185 &path[1..]
186 } else if path.starts_with('#') {
187 &path[1..]
188 } else {
189 path
190 };
191
192 clean_path.replace('/', ".")
194}
195
196pub fn canonicalize_schema_path(path: &str) -> Cow<'_, str> {
206 if path.is_empty() {
207 return Cow::Borrowed("");
208 }
209
210 if path.starts_with("/$") && !path.contains('.') && !path.contains("//") {
212 return Cow::Borrowed(path);
213 }
214
215 let is_system = path.starts_with('$') || path.starts_with("/$") || path.starts_with("#/$");
217
218 let clean_path = if path.starts_with("#/") {
220 &path[2..]
221 } else if path.starts_with('/') {
222 &path[1..]
223 } else if path.starts_with('#') {
224 &path[1..]
225 } else {
226 path
227 };
228
229 if !is_system
232 && !clean_path.contains('.')
233 && !clean_path.contains('/')
234 && !clean_path.is_empty()
235 {
236 if path.starts_with('/') && path.len() == clean_path.len() + 1 {
237 return Cow::Borrowed(path);
238 }
239 let mut s = String::with_capacity(clean_path.len() + 1);
240 s.push('/');
241 s.push_str(clean_path);
242 return Cow::Owned(s);
243 }
244
245 if clean_path.starts_with("properties/") || clean_path.starts_with("properties.")
247 || clean_path.contains("/properties/") || clean_path.contains(".properties.") {
248
249 let mut s = String::with_capacity(clean_path.len() + 1);
250 s.push('/');
251 for c in clean_path.chars() {
252 if c == '.' {
253 s.push('/');
254 } else {
255 s.push(c);
256 }
257 }
258 if s == path {
259 return Cow::Borrowed(path);
260 } else {
261 return Cow::Owned(s);
262 }
263 }
264
265 let mut result = String::with_capacity(path.len() * 2);
267 result.push('/');
268
269 let parts = clean_path.split(|c| c == '/' || c == '.');
270 let mut first = true;
271
272 for part in parts {
273 if part.is_empty() || part == "properties" {
274 continue;
275 }
276
277 if !first && !is_system {
278 result.push_str("properties/");
279 }
280 result.push_str(part);
281 result.push('/');
282 first = false;
283 }
284
285 if result.len() > 1 {
286 result.pop(); }
288
289 if result == path {
291 Cow::Borrowed(path)
292 } else {
293 Cow::Owned(result)
294 }
295}
296
297#[inline]
301pub fn get_value_by_pointer<'a>(data: &'a Value, pointer: &str) -> Option<&'a Value> {
302 if pointer.is_empty() {
303 Some(data)
304 } else {
305 data.pointer(pointer)
306 }
307}
308
309#[inline]
310pub fn get_value_by_pointer_without_properties<'a>(
311 data: &'a Value,
312 pointer: &str,
313) -> Option<&'a Value> {
314 if pointer.is_empty() {
315 Some(data)
316 } else {
317 data.pointer(&pointer.replace("properties/", ""))
318 }
319}
320
321pub fn get_values_by_pointers<'a>(data: &'a Value, pointers: &[String]) -> Vec<Option<&'a Value>> {
323 pointers
324 .iter()
325 .map(|pointer| get_value_by_pointer(data, pointer))
326 .collect()
327}
328
329#[inline]
333pub fn get_array_element<'a>(data: &'a Value, index: usize) -> Option<&'a Value> {
334 data.as_array()?.get(index)
335}
336
337#[inline]
341pub fn get_array_element_by_pointer<'a>(
342 data: &'a Value,
343 pointer: &str,
344 index: usize,
345) -> Option<&'a Value> {
346 get_value_by_pointer(data, pointer)?.as_array()?.get(index)
347}
348
349#[derive(Debug, Clone)]
351pub struct ArrayMetadata {
352 pub pointer: String,
354 pub length: usize,
356 pub column_names: Vec<String>,
358 pub is_uniform: bool,
360}
361
362impl ArrayMetadata {
363 pub fn build(data: &Value, pointer: &str) -> Option<Self> {
365 let array = get_value_by_pointer(data, pointer)?.as_array()?;
366
367 let length = array.len();
368 if length == 0 {
369 return Some(ArrayMetadata {
370 pointer: pointer.to_string(),
371 length: 0,
372 column_names: Vec::new(),
373 is_uniform: true,
374 });
375 }
376
377 let first_element = &array[0];
379 let column_names = if let Value::Object(obj) = first_element {
380 obj.keys().cloned().collect()
381 } else {
382 Vec::new()
383 };
384
385 let is_uniform = if !column_names.is_empty() {
387 array.iter().all(|elem| {
388 if let Value::Object(obj) = elem {
389 obj.keys().len() == column_names.len()
390 && column_names.iter().all(|col| obj.contains_key(col))
391 } else {
392 false
393 }
394 })
395 } else {
396 let first_type = std::mem::discriminant(first_element);
398 array
399 .iter()
400 .all(|elem| std::mem::discriminant(elem) == first_type)
401 };
402
403 Some(ArrayMetadata {
404 pointer: pointer.to_string(),
405 length,
406 column_names,
407 is_uniform,
408 })
409 }
410
411 #[inline]
413 pub fn get_column_value<'a>(
414 &self,
415 data: &'a Value,
416 row_index: usize,
417 column: &str,
418 ) -> Option<&'a Value> {
419 if !self.is_uniform || row_index >= self.length {
420 return None;
421 }
422
423 get_array_element_by_pointer(data, &self.pointer, row_index)?
424 .as_object()?
425 .get(column)
426 }
427
428 #[inline]
430 pub fn is_valid_index(&self, index: usize) -> bool {
431 index < self.length
432 }
433}