#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/dot-prop/0.1.0")]
#![allow(clippy::cast_possible_truncation)]
use core::fmt;
use serde_json::{Map, Value};
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
const DISALLOWED_KEYS: [&str; 3] = ["__proto__", "prototype", "constructor"];
const MAX_ARRAY_INDEX: u64 = 1_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Key(String),
Index(usize),
}
impl Segment {
fn as_object_key(&self) -> String {
match self {
Segment::Key(key) => key.clone(),
Segment::Index(index) => index.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsePathError {
message: String,
}
impl fmt::Display for ParsePathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ParsePathError {}
fn coerce_to_index(segment: &str) -> Option<usize> {
if segment == "0" {
return Some(0);
}
let bytes = segment.as_bytes();
let leads_nonzero = matches!(bytes.first(), Some(b'1'..=b'9'));
if leads_nonzero && bytes.iter().all(u8::is_ascii_digit) {
if let Ok(number) = segment.parse::<u64>() {
if number <= MAX_ARRAY_INDEX {
return Some(number as usize);
}
}
}
None
}
fn process_segment(segment: &str, parts: &mut Vec<Segment>) -> bool {
if DISALLOWED_KEYS.contains(&segment) {
return false;
}
if !segment.is_empty() {
if let Some(index) = coerce_to_index(segment) {
parts.push(Segment::Index(index));
return true;
}
}
parts.push(Segment::Key(segment.to_string()));
true
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Part {
Start,
Property,
Index,
IndexEnd,
}
#[allow(clippy::too_many_lines)] pub fn parse_path(path: &str) -> Result<Vec<Segment>, ParsePathError> {
let mut parts: Vec<Segment> = Vec::new();
let mut current_segment = String::new();
let mut current_part = Part::Start;
let mut is_escaping = false;
let mut position = 0usize;
let err = |message: String| Err(ParsePathError { message });
for character in path.chars() {
position += 1;
if is_escaping {
current_segment.push(character);
is_escaping = false;
continue;
}
if character == '\\' {
if current_part == Part::Index {
return err(format!(
"Invalid character '{character}' in an index at position {position}"
));
}
if current_part == Part::IndexEnd {
return err(format!(
"Invalid character '{character}' after an index at position {position}"
));
}
is_escaping = true;
if current_part == Part::Start {
current_part = Part::Property;
}
continue;
}
match character {
'.' => {
if current_part == Part::Index {
return err(format!(
"Invalid character '{character}' in an index at position {position}"
));
}
if current_part == Part::IndexEnd {
current_part = Part::Property;
continue;
}
if !process_segment(¤t_segment, &mut parts) {
return Ok(Vec::new());
}
current_segment.clear();
current_part = Part::Property;
}
'[' => {
if current_part == Part::Index {
return err(format!(
"Invalid character '{character}' in an index at position {position}"
));
}
if current_part == Part::IndexEnd {
current_part = Part::Index;
continue;
}
if current_part == Part::Property || current_part == Part::Start {
if (!current_segment.is_empty() || current_part == Part::Property)
&& !process_segment(¤t_segment, &mut parts)
{
return Ok(Vec::new());
}
current_segment.clear();
}
current_part = Part::Index;
}
']' if current_part == Part::Index => {
if current_segment.is_empty() {
let last = match parts.pop() {
Some(Segment::Key(key)) if !key.is_empty() => key,
Some(Segment::Index(index)) if index != 0 => index.to_string(),
_ => String::new(),
};
current_segment = format!("{last}[]");
current_part = Part::Property;
} else {
match current_segment.parse::<u64>() {
Ok(number)
if number <= MAX_ARRAY_INDEX
&& current_segment == number.to_string() =>
{
parts.push(Segment::Index(number as usize));
}
_ => parts.push(Segment::Key(current_segment.clone())),
}
current_segment.clear();
current_part = Part::IndexEnd;
}
}
']' if current_part == Part::IndexEnd => {
return err(format!(
"Invalid character '{character}' after an index at position {position}"
));
}
']' => {
current_segment.push(character);
}
_ => {
if current_part == Part::Index && !character.is_ascii_digit() {
return err(format!(
"Invalid character '{character}' in an index at position {position}"
));
}
if current_part == Part::IndexEnd {
return err(format!(
"Invalid character '{character}' after an index at position {position}"
));
}
if current_part == Part::Start {
current_part = Part::Property;
}
current_segment.push(character);
}
}
}
if is_escaping {
current_segment.push('\\');
}
match current_part {
Part::Property => {
if !process_segment(¤t_segment, &mut parts) {
return Ok(Vec::new());
}
}
Part::Index => return err("Index was not closed".to_string()),
Part::Start => parts.push(Segment::Key(String::new())),
Part::IndexEnd => {}
}
Ok(parts)
}
fn is_container(value: &Value) -> bool {
value.is_object() || value.is_array()
}
fn segment_get<'a>(value: &'a Value, segment: &Segment) -> Option<&'a Value> {
match value {
Value::Object(map) => map.get(&segment.as_object_key()),
Value::Array(array) => match segment {
Segment::Index(index) => array.get(*index),
Segment::Key(_) => None,
},
_ => None,
}
}
#[must_use]
pub fn get_property<'a>(object: &'a Value, path: &str) -> Option<&'a Value> {
if !is_container(object) {
return Some(object);
}
let Ok(segments) = parse_path(path) else {
return None;
};
if segments.is_empty() {
return None;
}
let mut current = object;
let last = segments.len() - 1;
for (index, segment) in segments.iter().enumerate() {
match segment_get(current, segment) {
None => return None,
Some(value) if value.is_null() => {
if index != last {
return None;
}
return Some(value);
}
Some(value) => current = value,
}
}
Some(current)
}
#[must_use]
pub fn has_property(object: &Value, path: &str) -> bool {
if !is_container(object) {
return false;
}
let Ok(segments) = parse_path(path) else {
return false;
};
if segments.is_empty() {
return false;
}
let mut current = object;
for segment in &segments {
let present = match current {
Value::Object(map) => map.contains_key(&segment.as_object_key()),
Value::Array(array) => match segment {
Segment::Index(index) => *index < array.len(),
Segment::Key(_) => false,
},
_ => return false,
};
if !present {
return false;
}
match segment_get(current, segment) {
Some(value) => current = value,
None => return false,
}
}
true
}
pub fn set_property(object: &mut Value, path: &str, value: Value) {
if !is_container(object) {
return;
}
let Ok(segments) = parse_path(path) else {
return;
};
if segments.is_empty() {
return;
}
set_segments(object, &segments, value);
}
fn set_segments(object: &mut Value, segments: &[Segment], value: Value) {
let (segment, rest) = segments.split_first().expect("non-empty");
if rest.is_empty() {
segment_set(object, segment, value);
return;
}
let next_is_index = matches!(rest[0], Segment::Index(_));
let needs_container = segment_get(object, segment).map_or(true, |child| !is_container(child));
if needs_container {
let new_child = if next_is_index {
Value::Array(Vec::new())
} else {
Value::Object(Map::new())
};
segment_set(object, segment, new_child);
}
if let Some(child) = segment_get_mut(object, segment) {
set_segments(child, rest, value);
}
}
fn segment_set(object: &mut Value, segment: &Segment, value: Value) {
match object {
Value::Object(map) => {
map.insert(segment.as_object_key(), value);
}
Value::Array(array) => {
if let Segment::Index(index) = segment {
if *index >= array.len() {
array.resize(*index + 1, Value::Null);
}
array[*index] = value;
}
}
_ => {}
}
}
fn segment_get_mut<'a>(object: &'a mut Value, segment: &Segment) -> Option<&'a mut Value> {
match object {
Value::Object(map) => map.get_mut(&segment.as_object_key()),
Value::Array(array) => match segment {
Segment::Index(index) => array.get_mut(*index),
Segment::Key(_) => None,
},
_ => None,
}
}
pub fn delete_property(object: &mut Value, path: &str) -> bool {
if !is_container(object) {
return false;
}
let Ok(segments) = parse_path(path) else {
return false;
};
if segments.is_empty() {
return false;
}
let last = segments.len() - 1;
let mut current = object;
for (index, segment) in segments.iter().enumerate() {
if index == last {
return delete_segment(current, segment);
}
match segment_get_mut(current, segment) {
Some(child) if is_container(child) => current = child,
_ => return false,
}
}
false
}
fn delete_segment(object: &mut Value, segment: &Segment) -> bool {
match object {
Value::Object(map) => map.remove(&segment.as_object_key()).is_some(),
Value::Array(array) => match segment {
Segment::Index(index) if *index < array.len() => {
array[*index] = Value::Null;
true
}
_ => false,
},
_ => false,
}
}
#[must_use]
pub fn escape_path(path: &str) -> String {
let mut out = String::with_capacity(path.len());
for c in path.chars() {
if matches!(c, '\\' | '.' | '[') {
out.push('\\');
}
out.push(c);
}
out
}
#[must_use]
pub fn stringify_path(segments: &[Segment]) -> String {
stringify_path_with(segments, false)
}
#[must_use]
pub fn stringify_path_with(segments: &[Segment], prefer_dot_for_indices: bool) -> String {
let mut out = String::new();
for (index, segment) in segments.iter().enumerate() {
match segment {
Segment::Index(number) => {
if prefer_dot_for_indices && index > 0 {
out.push('.');
out.push_str(&number.to_string());
} else {
out.push('[');
out.push_str(&number.to_string());
out.push(']');
}
}
Segment::Key(key) if key.is_empty() => {
if index != 0 {
out.push('.');
}
}
Segment::Key(key) => {
if let Some(number) = coerce_to_index(key) {
if prefer_dot_for_indices && index > 0 {
out.push('.');
out.push_str(&number.to_string());
} else {
out.push('[');
out.push_str(&number.to_string());
out.push(']');
}
} else {
let escaped = escape_path(key);
if index != 0 {
out.push('.');
}
out.push_str(&escaped);
}
}
}
}
out
}
fn normalized_entries(value: &Value) -> Vec<(Segment, &Value)> {
match value {
Value::Object(map) => map
.iter()
.map(|(key, entry)| {
let segment =
coerce_to_index(key).map_or_else(|| Segment::Key(key.clone()), Segment::Index);
(segment, entry)
})
.collect(),
Value::Array(array) => array
.iter()
.enumerate()
.map(|(index, entry)| (Segment::Index(index), entry))
.collect(),
_ => Vec::new(),
}
}
fn deep_keys_into(value: &Value, current_path: &mut Vec<Segment>, out: &mut Vec<String>) {
let is_empty = match value {
Value::Object(map) => map.is_empty(),
Value::Array(array) => array.is_empty(),
_ => true,
};
if !is_container(value) || is_empty {
if !current_path.is_empty() {
out.push(stringify_path(current_path));
}
return;
}
for (segment, entry) in normalized_entries(value) {
current_path.push(segment);
deep_keys_into(entry, current_path, out);
current_path.pop();
}
}
#[must_use]
pub fn deep_keys(object: &Value) -> Vec<String> {
let mut out = Vec::new();
let mut path = Vec::new();
deep_keys_into(object, &mut path, &mut out);
out
}
#[must_use]
pub fn unflatten(object: &Value) -> Value {
let mut result = Value::Object(Map::new());
if let Value::Object(map) = object {
for (path, value) in map {
set_property(&mut result, path, value.clone());
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_basic() {
assert_eq!(
parse_path("a.b.c").unwrap(),
vec![
Segment::Key("a".into()),
Segment::Key("b".into()),
Segment::Key("c".into())
]
);
assert_eq!(
parse_path("a[0].b").unwrap(),
vec![
Segment::Key("a".into()),
Segment::Index(0),
Segment::Key("b".into())
]
);
assert_eq!(
parse_path("foo\\.bar").unwrap(),
vec![Segment::Key("foo.bar".into())]
);
assert_eq!(parse_path("__proto__").unwrap(), vec![]);
assert_eq!(
parse_path("a[01]").unwrap(),
vec![Segment::Key("a".into()), Segment::Key("01".into())]
);
assert!(parse_path("a[b]").is_err());
assert!(parse_path("a[").is_err());
}
#[test]
fn get() {
let value = json!({ "foo": { "bar": [1, 2, 3] }, "n": null });
assert_eq!(get_property(&value, "foo.bar.1"), Some(&json!(2)));
assert_eq!(get_property(&value, "foo.bar[2]"), Some(&json!(3)));
assert_eq!(get_property(&value, "foo.missing"), None);
assert_eq!(get_property(&value, "n"), Some(&json!(null)));
assert_eq!(get_property(&value, "n.x"), None);
}
#[test]
fn set() {
let mut value = json!({});
set_property(&mut value, "a.b.0", json!("x"));
assert_eq!(value, json!({ "a": { "b": ["x"] } }));
set_property(&mut value, "a.b.2", json!("z"));
assert_eq!(value, json!({ "a": { "b": ["x", null, "z"] } }));
}
#[test]
fn has_and_delete() {
let mut value = json!({ "a": { "b": [1, 2] } });
assert!(has_property(&value, "a.b.0"));
assert!(!has_property(&value, "a.b.5"));
assert!(delete_property(&mut value, "a.b.0"));
assert_eq!(value, json!({ "a": { "b": [null, 2] } }));
assert!(!delete_property(&mut value, "a.b.9"));
}
#[test]
fn keys_and_unflatten() {
let value = json!({ "a": { "b": 1 }, "c": [2, 3] });
assert_eq!(deep_keys(&value), ["a.b", "c[0]", "c[1]"]);
assert_eq!(
unflatten(&json!({ "a.b": 1, "a.c": 2 })),
json!({ "a": { "b": 1, "c": 2 } })
);
}
#[test]
fn escape_and_stringify() {
assert_eq!(escape_path("foo.bar[0]"), "foo\\.bar\\[0]");
assert_eq!(
stringify_path(&[
Segment::Key("a".into()),
Segment::Index(0),
Segment::Key("b".into())
]),
"a[0].b"
);
}
}