use std::fmt;
use crate::CompactString;
use crate::value::ArgStrings;
use crate::value::json_path::JsonPath;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PropKey {
Name(CompactString),
Path(JsonPath),
}
impl PropKey {
pub fn as_name(&self) -> Option<&str> {
match self {
PropKey::Name(n) => Some(n.as_str()),
PropKey::Path(_) => None,
}
}
pub fn as_path(&self) -> Option<&JsonPath> {
match self {
PropKey::Path(p) => Some(p),
PropKey::Name(_) => None,
}
}
}
impl fmt::Display for PropKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropKey::Name(n) => f.write_str(n),
PropKey::Path(p) => fmt::Display::fmt(p, f),
}
}
}
impl From<&str> for PropKey {
fn from(s: &str) -> Self {
PropKey::Name(CompactString::new(s))
}
}
impl From<String> for PropKey {
fn from(s: String) -> Self {
PropKey::Name(CompactString::from(s))
}
}
impl From<&String> for PropKey {
fn from(s: &String) -> Self {
PropKey::Name(CompactString::new(s))
}
}
impl From<CompactString> for PropKey {
fn from(s: CompactString) -> Self {
PropKey::Name(s)
}
}
impl From<JsonPath> for PropKey {
fn from(p: JsonPath) -> Self {
PropKey::Path(p)
}
}
impl From<&JsonPath> for PropKey {
fn from(p: &JsonPath) -> Self {
PropKey::Path(p.clone())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PropKeys {
None,
One(PropKey),
List(Vec<PropKey>),
}
impl PropKeys {
pub(crate) fn into_names(self) -> Result<Option<Vec<String>>, Vec<PropKey>> {
let keys = match self {
PropKeys::None => return Ok(None),
PropKeys::One(key) => vec![key],
PropKeys::List(keys) => keys,
};
if keys.iter().all(|k| matches!(k, PropKey::Name(_))) {
Ok(Some(keys.into_iter().map(|k| k.to_string()).collect()))
} else {
Err(keys)
}
}
}
impl<T: Into<ArgStrings>> From<T> for PropKeys {
fn from(value: T) -> Self {
match value.into() {
ArgStrings::String(s) => PropKeys::One(PropKey::from(s)),
ArgStrings::List(list) => PropKeys::List(list.into_iter().map(PropKey::from).collect()),
ArgStrings::None => PropKeys::None,
}
}
}
impl From<JsonPath> for PropKeys {
fn from(p: JsonPath) -> Self {
PropKeys::One(PropKey::Path(p))
}
}
impl From<&JsonPath> for PropKeys {
fn from(p: &JsonPath) -> Self {
PropKeys::One(PropKey::from(p))
}
}
impl From<PropKey> for PropKeys {
fn from(k: PropKey) -> Self {
PropKeys::One(k)
}
}
impl From<Vec<PropKey>> for PropKeys {
fn from(keys: Vec<PropKey>) -> Self {
PropKeys::List(keys)
}
}
impl From<Vec<JsonPath>> for PropKeys {
fn from(keys: Vec<JsonPath>) -> Self {
PropKeys::List(keys.into_iter().map(PropKey::Path).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn path(s: &str) -> JsonPath {
s.parse().unwrap()
}
#[test]
fn from_conversions() {
assert_eq!(PropKey::from("a"), PropKey::Name("a".into()));
assert_eq!(PropKey::from(String::from("a")), PropKey::from("a"));
assert_eq!(PropKey::from(&String::from("a")), PropKey::from("a"));
assert_eq!(PropKey::from(CompactString::new("a")), PropKey::from("a"));
let p = path("a.b[1]");
assert_eq!(PropKey::from(&p), PropKey::from(p.clone()));
assert!(matches!(PropKey::from(p), PropKey::Path(_)));
}
#[test]
fn plain_string_with_dots_stays_a_name() {
let k = PropKey::from("a.b[1]");
assert_eq!(k.as_name(), Some("a.b[1]"));
assert!(k.as_path().is_none());
}
#[test]
fn display_name_and_canonical_path() {
assert_eq!(PropKey::from("a.b").to_string(), "a.b");
assert_eq!(PropKey::from(path("a.b[1].c")).to_string(), "$.a.b[1].c");
assert_eq!(
PropKey::from(path("$['x y'][-1]")).to_string(),
"$['x y'][-1]"
);
}
#[test]
fn name_and_path_are_distinct_keys() {
let name = PropKey::from("$.a");
let p = PropKey::from(path("a"));
assert_ne!(name, p);
assert_eq!(p, PropKey::from(path("$.a")));
let set: HashSet<PropKey> = [name, p.clone(), PropKey::from(path("$.a"))]
.into_iter()
.collect();
assert_eq!(set.len(), 2);
assert_eq!(p.as_path().map(|x| x.to_string()), Some("$.a".to_string()));
}
#[test]
fn clone_of_path_shares_segments() {
let p = PropKey::from(path("a.b"));
let q = p.clone();
match (&p, &q) {
(PropKey::Path(a), PropKey::Path(b)) => assert!(std::sync::Arc::ptr_eq(&a.0, &b.0)),
_ => panic!("expected paths"),
}
}
}