use alloc::{string::String, vec::Vec};
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocationSegment {
Property(String),
Index(usize),
}
impl fmt::Display for LocationSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Property(name) => write!(f, "{name}"),
Self::Index(idx) => write!(f, "{idx}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
segments: Vec<LocationSegment>,
}
impl Location {
#[must_use]
pub fn new() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn push_property(&mut self, name: impl Into<String>) {
self.segments.push(LocationSegment::Property(name.into()));
}
pub fn push_index(&mut self, idx: usize) {
self.segments.push(LocationSegment::Index(idx));
}
pub fn pop(&mut self) -> bool {
self.segments.pop().is_some()
}
#[must_use]
pub fn segments(&self) -> &[LocationSegment] {
&self.segments
}
#[must_use]
pub fn as_json_pointer(&self) -> String {
if self.segments.is_empty() {
return String::new();
}
let mut result = String::new();
for seg in &self.segments {
result.push('/');
match seg {
LocationSegment::Property(name) => {
let chars = name.chars();
for c in chars {
match c {
'~' => result.push_str("~0"),
'/' => result.push_str("~1"),
_ => result.push(c),
}
}
}
LocationSegment::Index(idx) => {
let mut buf = itoa::Buffer::new();
let s = buf.format(*idx);
result.push_str(s);
}
}
}
result
}
}
impl Default for Location {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pointer = self.as_json_pointer();
if pointer.is_empty() {
write!(f, "/")
} else {
write!(f, "{pointer}")
}
}
}
#[derive(Debug, Clone)]
pub struct LazyLocation<'a> {
parent: Option<&'a LazyLocation<'a>>,
segment: Option<LocationSegment>,
}
impl<'a> LazyLocation<'a> {
#[must_use]
pub fn new() -> Self {
Self {
parent: None,
segment: None,
}
}
#[must_use]
pub fn push_property(&'a self, name: &str) -> LazyLocation<'a> {
LazyLocation {
parent: Some(self),
segment: Some(LocationSegment::Property(name.into())),
}
}
#[must_use]
pub fn push_index(&'a self, idx: usize) -> LazyLocation<'a> {
LazyLocation {
parent: Some(self),
segment: Some(LocationSegment::Index(idx)),
}
}
#[must_use]
pub fn materialize(&self) -> Location {
let mut segments = Vec::new();
let mut current = self;
loop {
if let Some(ref seg) = current.segment {
segments.push(seg.clone());
}
match current.parent {
Some(parent) => current = parent,
None => break,
}
}
segments.reverse();
Location { segments }
}
}
impl Default for LazyLocation<'_> {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for LazyLocation<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let location = self.materialize();
write!(f, "{location}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_location_empty() {
let loc = Location::new();
assert!(loc.as_json_pointer().is_empty());
}
#[test]
fn test_location_push_property() {
let mut loc = Location::new();
loc.push_property("foo");
loc.push_property("bar");
assert_eq!(loc.as_json_pointer(), "/foo/bar");
}
#[test]
fn test_location_push_index() {
let mut loc = Location::new();
loc.push_property("items");
loc.push_index(3);
assert_eq!(loc.as_json_pointer(), "/items/3");
}
#[test]
fn test_location_pop() {
let mut loc = Location::new();
loc.push_property("foo");
loc.push_property("bar");
assert!(loc.pop());
assert_eq!(loc.as_json_pointer(), "/foo");
}
#[test]
fn test_location_json_pointer_escaping() {
let mut loc = Location::new();
loc.push_property("foo/bar");
loc.push_property("~baz");
loc.push_index(0);
assert_eq!(loc.as_json_pointer(), "/foo~1bar/~0baz/0");
}
#[test]
fn test_location_display() {
let mut loc = Location::new();
loc.push_property("foo");
assert_eq!(format!("{loc}"), "/foo");
}
#[test]
fn test_location_display_root() {
let loc = Location::new();
assert_eq!(format!("{loc}"), "/");
}
#[test]
fn test_lazy_location_materialize() {
let root = LazyLocation::new();
let a = root.push_property("a");
let idx = a.push_index(0);
let b = idx.push_property("b");
let loc = b.materialize();
assert_eq!(loc.as_json_pointer(), "/a/0/b");
}
#[test]
fn test_lazy_location_root_materialize() {
let root = LazyLocation::new();
let loc = root.materialize();
assert!(loc.as_json_pointer().is_empty());
}
#[test]
fn test_lazy_location_display() {
let root = LazyLocation::new();
let foo = root.push_property("foo");
assert_eq!(format!("{foo}"), "/foo");
}
}