pub(crate) mod parse;
mod resolve;
pub use resolve::{PatchError, ResolveError, ResolveErrorKind, ValueShape};
mod names;
pub use names::FieldNames;
mod value;
pub use value::{
Fields, FloatBits, MapKey, NamedPath, Nameless, NamelessKind, ValuePath, ValueSegment,
};
#[cfg(test)]
mod tests;
use std::{
borrow::{Borrow, Cow},
fmt::{self, Write as _},
str::FromStr,
};
use ltk_hash::{BinHash, Hash as _};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PropertyPath(String);
impl PropertyPath {
pub const MAX_LEN: usize = u16::MAX as usize;
pub fn new(path: impl Into<String>) -> Result<Self, PropertyPathError> {
let path = path.into();
parse::validate(&path)?;
Ok(Self(path))
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
false
}
#[must_use]
#[inline]
pub fn segments(&self) -> Segments<'_> {
Segments(parse::Parser::new(&self.0))
}
pub fn push_field(&mut self, name: &str) -> Result<(), PropertyPathError> {
let at = self.0.len() + 1;
if name.is_empty() {
return Err(PropertyPathError::new(
at,
PropertyPathErrorKind::EmptySegment,
));
}
if let Some((offset, c)) = name.char_indices().find(|&(_, c)| !parse::is_name_char(c)) {
return Err(PropertyPathError::new(
at + offset,
PropertyPathErrorKind::UnexpectedCharacter(c),
));
}
self.push(format_args!(".{name}"))
}
pub fn push_index(&mut self, index: u32) -> Result<(), PropertyPathError> {
self.push(format_args!("[{index}]"))
}
pub fn push_key(&mut self, key: &KeyLiteral<'_>) -> Result<(), PropertyPathError> {
self.push(format_args!("{{{key}}}"))
}
fn push(&mut self, piece: fmt::Arguments<'_>) -> Result<(), PropertyPathError> {
let candidate = format!("{}{}", self.0, piece);
parse::validate(&candidate)?;
self.0 = candidate;
Ok(())
}
}
impl fmt::Display for PropertyPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for PropertyPath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for PropertyPath {
fn borrow(&self) -> &str {
&self.0
}
}
impl FromStr for PropertyPath {
type Err = PropertyPathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<String> for PropertyPath {
type Error = PropertyPathError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for PropertyPath {
type Error = PropertyPathError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<PropertyPath> for String {
fn from(value: PropertyPath) -> Self {
value.0
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for PropertyPath {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PropertyPath {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let path = <String as serde::Deserialize>::deserialize(deserializer)?;
Self::new(path).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug)]
pub struct Segments<'a>(parse::Parser<'a>);
impl<'a> Iterator for Segments<'a> {
type Item = Segment<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.0
.next_segment()
.transpose()
.expect("a validated PropertyPath always re-parses")
}
}
impl std::iter::FusedIterator for Segments<'_> {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Segment<'a> {
pub name: &'a str,
pub subscript: Option<Subscript<'a>>,
}
impl Segment<'_> {
#[must_use]
#[inline]
pub fn name_hash(&self) -> BinHash {
BinHash::hash_str(self.name)
}
}
impl fmt::Display for Segment<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name)?;
match &self.subscript {
Some(subscript) => write!(f, "{subscript}"),
None => Ok(()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Subscript<'a> {
Index(u32),
Key(KeyLiteral<'a>),
}
impl fmt::Display for Subscript<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Index(index) => write!(f, "[{index}]"),
Self::Key(key) => write!(f, "{{{key}}}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeyLiteral<'a> {
Bool(bool),
Number(&'a str),
String(Cow<'a, str>),
}
impl<'a> From<&'a str> for KeyLiteral<'a> {
fn from(value: &'a str) -> Self {
Self::String(Cow::Borrowed(value))
}
}
impl From<bool> for KeyLiteral<'_> {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl fmt::Display for KeyLiteral<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bool(true) => f.write_str("true"),
Self::Bool(false) => f.write_str("false"),
Self::Number(text) => f.write_str(text),
Self::String(text) => {
f.write_char('"')?;
for c in text.chars() {
match c {
'"' => f.write_str("\\\"")?,
'\\' => f.write_str("\\\\")?,
'\u{8}' => f.write_str("\\b")?,
'\u{c}' => f.write_str("\\f")?,
'\n' => f.write_str("\\n")?,
'\r' => f.write_str("\\r")?,
'\t' => f.write_str("\\t")?,
c if (c as u32) < 0x20 => write!(f, "\\u{:04x}", c as u32)?,
c => f.write_char(c)?,
}
}
f.write_char('"')
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{kind} at byte {offset}")]
pub struct PropertyPathError {
offset: usize,
kind: PropertyPathErrorKind,
}
impl PropertyPathError {
pub(crate) fn new(offset: usize, kind: PropertyPathErrorKind) -> Self {
Self { offset, kind }
}
#[must_use]
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
#[must_use]
#[inline]
pub fn kind(&self) -> PropertyPathErrorKind {
self.kind
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PropertyPathErrorKind {
#[error("expected a property name")]
EmptySegment,
#[error("unexpected character {0:?}")]
UnexpectedCharacter(char),
#[error("unbalanced bracket")]
UnbalancedBracket,
#[error("a segment can only have one subscript")]
DoubleSubscript,
#[error("expected a non-negative integer index")]
InvalidIndex,
#[error("expected a JSON number, string or boolean key")]
InvalidKey,
#[error("path is {0} bytes, the limit is 65535")]
TooLong(usize),
}