1#![forbid(unsafe_code)]
28#![doc(html_root_url = "https://docs.rs/dot-prop/0.1.0")]
29#![allow(clippy::cast_possible_truncation)]
31
32use core::fmt;
33use serde_json::{Map, Value};
34
35#[cfg(doctest)]
37#[doc = include_str!("../README.md")]
38struct ReadmeDoctests;
39
40const DISALLOWED_KEYS: [&str; 3] = ["__proto__", "prototype", "constructor"];
42
43const MAX_ARRAY_INDEX: u64 = 1_000_000;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum Segment {
49 Key(String),
51 Index(usize),
53}
54
55impl Segment {
56 fn as_object_key(&self) -> String {
58 match self {
59 Segment::Key(key) => key.clone(),
60 Segment::Index(index) => index.to_string(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ParsePathError {
68 message: String,
69}
70
71impl fmt::Display for ParsePathError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(&self.message)
74 }
75}
76
77impl std::error::Error for ParsePathError {}
78
79fn coerce_to_index(segment: &str) -> Option<usize> {
81 if segment == "0" {
82 return Some(0);
83 }
84 let bytes = segment.as_bytes();
85 let leads_nonzero = matches!(bytes.first(), Some(b'1'..=b'9'));
86 if leads_nonzero && bytes.iter().all(u8::is_ascii_digit) {
87 if let Ok(number) = segment.parse::<u64>() {
88 if number <= MAX_ARRAY_INDEX {
89 return Some(number as usize);
90 }
91 }
92 }
93 None
94}
95
96fn process_segment(segment: &str, parts: &mut Vec<Segment>) -> bool {
99 if DISALLOWED_KEYS.contains(&segment) {
100 return false;
101 }
102 if !segment.is_empty() {
103 if let Some(index) = coerce_to_index(segment) {
104 parts.push(Segment::Index(index));
105 return true;
106 }
107 }
108 parts.push(Segment::Key(segment.to_string()));
109 true
110}
111
112#[derive(Clone, Copy, PartialEq, Eq)]
113enum Part {
114 Start,
115 Property,
116 Index,
117 IndexEnd,
118}
119
120#[allow(clippy::too_many_lines)] pub fn parse_path(path: &str) -> Result<Vec<Segment>, ParsePathError> {
127 let mut parts: Vec<Segment> = Vec::new();
128 let mut current_segment = String::new();
129 let mut current_part = Part::Start;
130 let mut is_escaping = false;
131 let mut position = 0usize;
132
133 let err = |message: String| Err(ParsePathError { message });
134
135 for character in path.chars() {
136 position += 1;
137
138 if is_escaping {
139 current_segment.push(character);
140 is_escaping = false;
141 continue;
142 }
143
144 if character == '\\' {
145 if current_part == Part::Index {
146 return err(format!(
147 "Invalid character '{character}' in an index at position {position}"
148 ));
149 }
150 if current_part == Part::IndexEnd {
151 return err(format!(
152 "Invalid character '{character}' after an index at position {position}"
153 ));
154 }
155 is_escaping = true;
156 if current_part == Part::Start {
157 current_part = Part::Property;
158 }
159 continue;
160 }
161
162 match character {
163 '.' => {
164 if current_part == Part::Index {
165 return err(format!(
166 "Invalid character '{character}' in an index at position {position}"
167 ));
168 }
169 if current_part == Part::IndexEnd {
170 current_part = Part::Property;
171 continue;
172 }
173 if !process_segment(¤t_segment, &mut parts) {
174 return Ok(Vec::new());
175 }
176 current_segment.clear();
177 current_part = Part::Property;
178 }
179 '[' => {
180 if current_part == Part::Index {
181 return err(format!(
182 "Invalid character '{character}' in an index at position {position}"
183 ));
184 }
185 if current_part == Part::IndexEnd {
186 current_part = Part::Index;
187 continue;
188 }
189 if current_part == Part::Property || current_part == Part::Start {
190 if (!current_segment.is_empty() || current_part == Part::Property)
191 && !process_segment(¤t_segment, &mut parts)
192 {
193 return Ok(Vec::new());
194 }
195 current_segment.clear();
196 }
197 current_part = Part::Index;
198 }
199 ']' if current_part == Part::Index => {
200 if current_segment.is_empty() {
201 let last = match parts.pop() {
203 Some(Segment::Key(key)) if !key.is_empty() => key,
204 Some(Segment::Index(index)) if index != 0 => index.to_string(),
205 _ => String::new(),
206 };
207 current_segment = format!("{last}[]");
208 current_part = Part::Property;
209 } else {
210 match current_segment.parse::<u64>() {
212 Ok(number)
213 if number <= MAX_ARRAY_INDEX
214 && current_segment == number.to_string() =>
215 {
216 parts.push(Segment::Index(number as usize));
217 }
218 _ => parts.push(Segment::Key(current_segment.clone())),
219 }
220 current_segment.clear();
221 current_part = Part::IndexEnd;
222 }
223 }
224 ']' if current_part == Part::IndexEnd => {
225 return err(format!(
226 "Invalid character '{character}' after an index at position {position}"
227 ));
228 }
229 ']' => {
230 current_segment.push(character);
232 }
233 _ => {
234 if current_part == Part::Index && !character.is_ascii_digit() {
235 return err(format!(
236 "Invalid character '{character}' in an index at position {position}"
237 ));
238 }
239 if current_part == Part::IndexEnd {
240 return err(format!(
241 "Invalid character '{character}' after an index at position {position}"
242 ));
243 }
244 if current_part == Part::Start {
245 current_part = Part::Property;
246 }
247 current_segment.push(character);
248 }
249 }
250 }
251
252 if is_escaping {
253 current_segment.push('\\');
254 }
255
256 match current_part {
257 Part::Property => {
258 if !process_segment(¤t_segment, &mut parts) {
259 return Ok(Vec::new());
260 }
261 }
262 Part::Index => return err("Index was not closed".to_string()),
263 Part::Start => parts.push(Segment::Key(String::new())),
264 Part::IndexEnd => {}
265 }
266
267 Ok(parts)
268}
269
270fn is_container(value: &Value) -> bool {
271 value.is_object() || value.is_array()
272}
273
274fn segment_get<'a>(value: &'a Value, segment: &Segment) -> Option<&'a Value> {
276 match value {
277 Value::Object(map) => map.get(&segment.as_object_key()),
278 Value::Array(array) => match segment {
279 Segment::Index(index) => array.get(*index),
280 Segment::Key(_) => None,
281 },
282 _ => None,
283 }
284}
285
286#[must_use]
296pub fn get_property<'a>(object: &'a Value, path: &str) -> Option<&'a Value> {
297 if !is_container(object) {
298 return Some(object);
300 }
301 let Ok(segments) = parse_path(path) else {
302 return None;
303 };
304 if segments.is_empty() {
305 return None;
306 }
307
308 let mut current = object;
309 let last = segments.len() - 1;
310 for (index, segment) in segments.iter().enumerate() {
311 match segment_get(current, segment) {
312 None => return None,
313 Some(value) if value.is_null() => {
314 if index != last {
315 return None;
316 }
317 return Some(value);
318 }
319 Some(value) => current = value,
320 }
321 }
322 Some(current)
323}
324
325#[must_use]
327pub fn has_property(object: &Value, path: &str) -> bool {
328 if !is_container(object) {
329 return false;
330 }
331 let Ok(segments) = parse_path(path) else {
332 return false;
333 };
334 if segments.is_empty() {
335 return false;
336 }
337
338 let mut current = object;
339 for segment in &segments {
340 let present = match current {
341 Value::Object(map) => map.contains_key(&segment.as_object_key()),
342 Value::Array(array) => match segment {
343 Segment::Index(index) => *index < array.len(),
344 Segment::Key(_) => false,
345 },
346 _ => return false,
347 };
348 if !present {
349 return false;
350 }
351 match segment_get(current, segment) {
352 Some(value) => current = value,
353 None => return false,
354 }
355 }
356 true
357}
358
359pub fn set_property(object: &mut Value, path: &str, value: Value) {
373 if !is_container(object) {
374 return;
375 }
376 let Ok(segments) = parse_path(path) else {
377 return;
378 };
379 if segments.is_empty() {
380 return;
381 }
382 set_segments(object, &segments, value);
383}
384
385fn set_segments(object: &mut Value, segments: &[Segment], value: Value) {
386 let (segment, rest) = segments.split_first().expect("non-empty");
387
388 if rest.is_empty() {
389 segment_set(object, segment, value);
390 return;
391 }
392
393 let next_is_index = matches!(rest[0], Segment::Index(_));
394 let needs_container = segment_get(object, segment).map_or(true, |child| !is_container(child));
395 if needs_container {
396 let new_child = if next_is_index {
397 Value::Array(Vec::new())
398 } else {
399 Value::Object(Map::new())
400 };
401 segment_set(object, segment, new_child);
402 }
403
404 if let Some(child) = segment_get_mut(object, segment) {
405 set_segments(child, rest, value);
406 }
407}
408
409fn segment_set(object: &mut Value, segment: &Segment, value: Value) {
411 match object {
412 Value::Object(map) => {
413 map.insert(segment.as_object_key(), value);
414 }
415 Value::Array(array) => {
416 if let Segment::Index(index) = segment {
417 if *index >= array.len() {
418 array.resize(*index + 1, Value::Null);
419 }
420 array[*index] = value;
421 }
422 }
424 _ => {}
425 }
426}
427
428fn segment_get_mut<'a>(object: &'a mut Value, segment: &Segment) -> Option<&'a mut Value> {
429 match object {
430 Value::Object(map) => map.get_mut(&segment.as_object_key()),
431 Value::Array(array) => match segment {
432 Segment::Index(index) => array.get_mut(*index),
433 Segment::Key(_) => None,
434 },
435 _ => None,
436 }
437}
438
439pub fn delete_property(object: &mut Value, path: &str) -> bool {
444 if !is_container(object) {
445 return false;
446 }
447 let Ok(segments) = parse_path(path) else {
448 return false;
449 };
450 if segments.is_empty() {
451 return false;
452 }
453
454 let last = segments.len() - 1;
455 let mut current = object;
456 for (index, segment) in segments.iter().enumerate() {
457 if index == last {
458 return delete_segment(current, segment);
459 }
460 match segment_get_mut(current, segment) {
461 Some(child) if is_container(child) => current = child,
462 _ => return false,
463 }
464 }
465 false
466}
467
468fn delete_segment(object: &mut Value, segment: &Segment) -> bool {
469 match object {
470 Value::Object(map) => map.remove(&segment.as_object_key()).is_some(),
471 Value::Array(array) => match segment {
472 Segment::Index(index) if *index < array.len() => {
473 array[*index] = Value::Null;
474 true
475 }
476 _ => false,
477 },
478 _ => false,
479 }
480}
481
482#[must_use]
489pub fn escape_path(path: &str) -> String {
490 let mut out = String::with_capacity(path.len());
491 for c in path.chars() {
492 if matches!(c, '\\' | '.' | '[') {
493 out.push('\\');
494 }
495 out.push(c);
496 }
497 out
498}
499
500#[must_use]
503pub fn stringify_path(segments: &[Segment]) -> String {
504 stringify_path_with(segments, false)
505}
506
507#[must_use]
510pub fn stringify_path_with(segments: &[Segment], prefer_dot_for_indices: bool) -> String {
511 let mut out = String::new();
512 for (index, segment) in segments.iter().enumerate() {
513 match segment {
514 Segment::Index(number) => {
515 if prefer_dot_for_indices && index > 0 {
516 out.push('.');
517 out.push_str(&number.to_string());
518 } else {
519 out.push('[');
520 out.push_str(&number.to_string());
521 out.push(']');
522 }
523 }
524 Segment::Key(key) if key.is_empty() => {
525 if index != 0 {
526 out.push('.');
527 }
528 }
529 Segment::Key(key) => {
530 if let Some(number) = coerce_to_index(key) {
531 if prefer_dot_for_indices && index > 0 {
532 out.push('.');
533 out.push_str(&number.to_string());
534 } else {
535 out.push('[');
536 out.push_str(&number.to_string());
537 out.push(']');
538 }
539 } else {
540 let escaped = escape_path(key);
541 if index != 0 {
542 out.push('.');
543 }
544 out.push_str(&escaped);
545 }
546 }
547 }
548 }
549 out
550}
551
552fn normalized_entries(value: &Value) -> Vec<(Segment, &Value)> {
554 match value {
555 Value::Object(map) => map
556 .iter()
557 .map(|(key, entry)| {
558 let segment =
559 coerce_to_index(key).map_or_else(|| Segment::Key(key.clone()), Segment::Index);
560 (segment, entry)
561 })
562 .collect(),
563 Value::Array(array) => array
564 .iter()
565 .enumerate()
566 .map(|(index, entry)| (Segment::Index(index), entry))
567 .collect(),
568 _ => Vec::new(),
569 }
570}
571
572fn deep_keys_into(value: &Value, current_path: &mut Vec<Segment>, out: &mut Vec<String>) {
573 let is_empty = match value {
574 Value::Object(map) => map.is_empty(),
575 Value::Array(array) => array.is_empty(),
576 _ => true,
577 };
578 if !is_container(value) || is_empty {
579 if !current_path.is_empty() {
580 out.push(stringify_path(current_path));
581 }
582 return;
583 }
584 for (segment, entry) in normalized_entries(value) {
585 current_path.push(segment);
586 deep_keys_into(entry, current_path, out);
587 current_path.pop();
588 }
589}
590
591#[must_use]
600pub fn deep_keys(object: &Value) -> Vec<String> {
601 let mut out = Vec::new();
602 let mut path = Vec::new();
603 deep_keys_into(object, &mut path, &mut out);
604 out
605}
606
607#[must_use]
615pub fn unflatten(object: &Value) -> Value {
616 let mut result = Value::Object(Map::new());
617 if let Value::Object(map) = object {
618 for (path, value) in map {
619 set_property(&mut result, path, value.clone());
620 }
621 }
622 result
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use serde_json::json;
629
630 #[test]
631 fn parse_basic() {
632 assert_eq!(
633 parse_path("a.b.c").unwrap(),
634 vec![
635 Segment::Key("a".into()),
636 Segment::Key("b".into()),
637 Segment::Key("c".into())
638 ]
639 );
640 assert_eq!(
641 parse_path("a[0].b").unwrap(),
642 vec![
643 Segment::Key("a".into()),
644 Segment::Index(0),
645 Segment::Key("b".into())
646 ]
647 );
648 assert_eq!(
649 parse_path("foo\\.bar").unwrap(),
650 vec![Segment::Key("foo.bar".into())]
651 );
652 assert_eq!(parse_path("__proto__").unwrap(), vec![]);
653 assert_eq!(
654 parse_path("a[01]").unwrap(),
655 vec![Segment::Key("a".into()), Segment::Key("01".into())]
656 );
657 assert!(parse_path("a[b]").is_err());
658 assert!(parse_path("a[").is_err());
659 }
660
661 #[test]
662 fn get() {
663 let value = json!({ "foo": { "bar": [1, 2, 3] }, "n": null });
664 assert_eq!(get_property(&value, "foo.bar.1"), Some(&json!(2)));
665 assert_eq!(get_property(&value, "foo.bar[2]"), Some(&json!(3)));
666 assert_eq!(get_property(&value, "foo.missing"), None);
667 assert_eq!(get_property(&value, "n"), Some(&json!(null)));
668 assert_eq!(get_property(&value, "n.x"), None);
669 }
670
671 #[test]
672 fn set() {
673 let mut value = json!({});
674 set_property(&mut value, "a.b.0", json!("x"));
675 assert_eq!(value, json!({ "a": { "b": ["x"] } }));
676 set_property(&mut value, "a.b.2", json!("z"));
677 assert_eq!(value, json!({ "a": { "b": ["x", null, "z"] } }));
678 }
679
680 #[test]
681 fn has_and_delete() {
682 let mut value = json!({ "a": { "b": [1, 2] } });
683 assert!(has_property(&value, "a.b.0"));
684 assert!(!has_property(&value, "a.b.5"));
685 assert!(delete_property(&mut value, "a.b.0"));
686 assert_eq!(value, json!({ "a": { "b": [null, 2] } }));
687 assert!(!delete_property(&mut value, "a.b.9"));
688 }
689
690 #[test]
691 fn keys_and_unflatten() {
692 let value = json!({ "a": { "b": 1 }, "c": [2, 3] });
693 assert_eq!(deep_keys(&value), ["a.b", "c[0]", "c[1]"]);
694 assert_eq!(
695 unflatten(&json!({ "a.b": 1, "a.c": 2 })),
696 json!({ "a": { "b": 1, "c": 2 } })
697 );
698 }
699
700 #[test]
701 fn escape_and_stringify() {
702 assert_eq!(escape_path("foo.bar[0]"), "foo\\.bar\\[0]");
703 assert_eq!(
704 stringify_path(&[
705 Segment::Key("a".into()),
706 Segment::Index(0),
707 Segment::Key("b".into())
708 ]),
709 "a[0].b"
710 );
711 }
712}