saddle-core 0.3.37

Shared contracts for Saddle components
Documentation
//! RFC 6901 selectors shared by context APIs and logging configuration.
//! Validation and matching borrow the selector; they allocate no decoded keys.

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvalidJsonPointer;

#[derive(Clone, Copy, Debug)]
pub struct JsonPointer<'a>(&'a str);

impl<'a> JsonPointer<'a> {
    pub fn parse(path: &'a str) -> Result<Self, InvalidJsonPointer> {
        if !path.is_empty() && !path.starts_with('/') {
            return Err(InvalidJsonPointer);
        }
        let mut bytes = path.bytes();
        while let Some(byte) = bytes.next() {
            if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) {
                return Err(InvalidJsonPointer);
            }
        }
        Ok(Self(path))
    }

    pub fn segments(self) -> impl Iterator<Item = PointerSegment<'a>> {
        self.0.split('/').skip(1).map(PointerSegment)
    }
}

#[derive(Clone, Copy)]
pub struct PointerSegment<'a>(&'a str);

impl PointerSegment<'_> {
    pub fn decode_into<'a>(self, target: &'a mut [u8]) -> Result<&'a str, InvalidJsonPointer> {
        let mut chars = self.0.chars();
        let mut used = 0;
        while let Some(ch) = chars.next() {
            let ch = if ch == '~' {
                match chars.next() { Some('0') => '~', Some('1') => '/', _ => return Err(InvalidJsonPointer) }
            } else { ch };
            let mut bytes = [0; 4];
            let bytes = ch.encode_utf8(&mut bytes).as_bytes();
            let end = used + bytes.len();
            target.get_mut(used..end).ok_or(InvalidJsonPointer)?.copy_from_slice(bytes);
            used = end;
        }
        std::str::from_utf8(&target[..used]).map_err(|_| InvalidJsonPointer)
    }
    pub fn matches(self, key: &str) -> bool {
        let mut encoded = self.0.chars();
        let decoded = std::iter::from_fn(move || {
            encoded.next().map(|ch| {
                if ch == '~' {
                    match encoded.next() {
                        Some('0') => '~',
                        Some('1') => '/',
                        _ => unreachable!("validated JSON Pointer"),
                    }
                } else {
                    ch
                }
            })
        });
        decoded.eq(key.chars())
    }

    pub fn array_index(self) -> Option<usize> {
        let raw = self.0;
        if raw.is_empty() || (raw.len() > 1 && raw.starts_with('0'))
            || !raw.bytes().all(|b| b.is_ascii_digit()) {
            return None;
        }
        raw.parse().ok()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn escaped_keys_empty_keys_and_canonical_array_indices() {
        let path = JsonPointer::parse("/a~1b/~0/界//0").unwrap();
        for (segment, key) in path.segments().zip(["a/b", "~", "界", "", "0"]) {
            assert!(segment.matches(key));
        }
        assert_eq!(JsonPointer::parse("").unwrap().segments().count(), 0);
        for path in ["a", "/~", "/~2", "/~~0"] {
            assert!(JsonPointer::parse(path).is_err());
        }
        for key in ["00", "01", "-", "-1", "+1", "", "184467440737095516160"] {
            assert!(PointerSegment(key).array_index().is_none());
        }
        assert_eq!(PointerSegment("0").array_index(), Some(0));
        assert_eq!(PointerSegment("12").array_index(), Some(12));
    }
}