miniconf/
jsonpath.rs

1use core::{fmt::Write, ops::ControlFlow::*};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{DescendError, IntoKeys, KeysIter, Schema, Transcode};
6
7/// JSON style path notation iterator
8///
9/// This is only styled after JSON notation, it does not adhere to it.
10/// Supported are both dot and key notation with and without
11/// names enclosed by `'` as well as various mixtures:
12///
13/// ```
14/// use miniconf::JsonPathIter;
15/// let path = ["foo", "bar", "4", "baz", "5", "6"];
16/// for valid in [
17///     ".foo.bar[4].baz[5][6]",
18///     "['foo']['bar'][4]['baz'][5][6]",
19///     ".foo['bar'].4.'baz'['5'].'6'",
20/// ] {
21///     assert_eq!(&path[..], JsonPathIter::new(valid).collect::<Vec<_>>());
22/// }
23///
24/// for short in ["'", "[", "['"] {
25///     assert!(JsonPathIter::new(short).next().is_none());
26/// }
27/// ```
28///
29/// # Limitations
30///
31/// * No attempt at validating conformance
32/// * Does not support any escaping
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash)]
34#[repr(transparent)]
35#[serde(transparent)]
36pub struct JsonPathIter<'a>(&'a str);
37
38impl<'a> JsonPathIter<'a> {
39    /// Interpret a str as a JSON path to be iterated over.
40    pub fn new(value: &'a str) -> Self {
41        Self(value)
42    }
43}
44
45impl core::fmt::Display for JsonPathIter<'_> {
46    #[inline]
47    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        self.0.fmt(f)
49    }
50}
51
52impl<'a> Iterator for JsonPathIter<'a> {
53    type Item = &'a str;
54
55    fn next(&mut self) -> Option<Self::Item> {
56        for (open, close) in [
57            (".'", Continue("'")),    // "'" inclusive
58            (".", Break(['.', '['])), // '.' or '[' exclusive
59            ("['", Continue("']")),   // "']" inclusive
60            ("[", Continue("]")),     // "]" inclusive
61        ] {
62            if let Some(rest) = self.0.strip_prefix(open) {
63                let (end, sep) = match close {
64                    Break(close) => (rest.find(close).unwrap_or(rest.len()), 0),
65                    Continue(close) => (rest.find(close)?, close.len()),
66                };
67                let (next, rest) = rest.split_at(end);
68                self.0 = &rest[sep..];
69                return Some(next);
70            }
71        }
72        None
73    }
74}
75
76impl core::iter::FusedIterator for JsonPathIter<'_> {}
77
78/// JSON style path notation
79///
80/// `T` can be `Write` for `Transcode` with the following behavior:
81/// * Named fields (struct) are encoded in dot notation.
82/// * Indices (tuple struct, array) are encoded in index notation
83///
84/// `T` can be `AsRef<str>` for `IntoKeys` with the behavior described in [`JsonPathIter`].
85#[derive(
86    Clone, Copy, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash,
87)]
88#[repr(transparent)]
89#[serde(transparent)]
90pub struct JsonPath<T: ?Sized>(pub T);
91
92impl<T> JsonPath<T> {
93    /// Extract the inner value
94    #[inline]
95    pub fn into_inner(self) -> T {
96        self.0
97    }
98}
99
100impl<T: core::fmt::Display> core::fmt::Display for JsonPath<T> {
101    #[inline]
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        self.0.fmt(f)
104    }
105}
106
107impl<'a, T: AsRef<str> + ?Sized> IntoKeys for &'a JsonPath<T> {
108    type IntoKeys = KeysIter<JsonPathIter<'a>>;
109    #[inline]
110    fn into_keys(self) -> Self::IntoKeys {
111        JsonPathIter(self.0.as_ref()).into_keys()
112    }
113}
114
115impl<T: Write + ?Sized> Transcode for JsonPath<T> {
116    type Error = core::fmt::Error;
117
118    fn transcode(
119        &mut self,
120        schema: &Schema,
121        keys: impl IntoKeys,
122    ) -> Result<(), DescendError<Self::Error>> {
123        schema.descend(keys.into_keys(), |_meta, idx_internal| {
124            if let Some((index, internal)) = idx_internal {
125                if let Some(name) = internal.get_name(index) {
126                    debug_assert!(!name.contains(['.', '\'', '[', ']']));
127                    self.0.write_char('.')?;
128                    self.0.write_str(name)?;
129                } else {
130                    self.0.write_char('[')?;
131                    self.0.write_str(itoa::Buffer::new().format(index))?;
132                    self.0.write_char(']')?;
133                }
134            }
135            Ok(())
136        })
137    }
138}