use std::borrow::Cow;
use std::fmt;
use crate::err::Error;
use crate::expr::part::Part;
use crate::expr::{Expr, Idiom, Literal};
use crate::val::{Set, Value};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FieldPathPart {
Field(String),
Index(usize),
First,
Last,
Lookup(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FieldPath(pub Vec<FieldPathPart>);
impl TryFrom<&Idiom> for FieldPath {
type Error = Error;
fn try_from(idiom: &Idiom) -> Result<Self, Self::Error> {
use surrealdb_types::ToSql;
let mut parts = Vec::with_capacity(idiom.len());
for part in idiom.iter() {
match part {
Part::Field(name) => parts.push(FieldPathPart::Field(name.as_str().to_owned())),
Part::First => parts.push(FieldPathPart::First),
Part::Last => parts.push(FieldPathPart::Last),
Part::Value(Expr::Literal(Literal::Integer(i))) if *i >= 0 => {
parts.push(FieldPathPart::Index(*i as usize))
}
Part::Lookup(lookup) => {
parts.push(FieldPathPart::Lookup(lookup.to_sql()))
}
Part::Destructure(_) | Part::Start(_) => {}
_ => {
return Err(Error::Query {
message: format!(
"FieldPath cannot contain complex parts like where clauses or method calls. \
Only simple field access (a.b.c), literal indices ([0], [$]), and graph traversals are supported. \
Got: {:?}",
idiom
),
});
}
}
}
Ok(FieldPath(parts))
}
}
impl FieldPath {
pub fn field(name: impl Into<String>) -> Self {
FieldPath(vec![FieldPathPart::Field(name.into())])
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn extract<'a>(&self, value: &'a Value) -> Cow<'a, Value> {
let mut current = Cow::Borrowed(value);
for part in &self.0 {
current = match current {
Cow::Borrowed(v) => match step(v, part) {
Step::Borrowed(b) => Cow::Borrowed(b),
Step::Owned(o) => Cow::Owned(o),
},
Cow::Owned(v) => Cow::Owned(match step(&v, part) {
Step::Borrowed(b) => b.clone(),
Step::Owned(o) => o,
}),
};
}
current
}
}
enum Step<'v> {
Borrowed(&'v Value),
Owned(Value),
}
fn step<'v>(value: &'v Value, part: &FieldPathPart) -> Step<'v> {
match (value, part) {
(Value::Object(obj), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
obj.get(name).map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Array(arr), FieldPathPart::Index(i)) => {
arr.get(*i).map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Set(set), FieldPathPart::Index(i)) => {
set.nth(*i).map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Array(arr), FieldPathPart::First) => {
arr.first().map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Set(set), FieldPathPart::First) => {
set.first().map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Array(arr), FieldPathPart::Last) => {
arr.last().map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Set(set), FieldPathPart::Last) => {
set.last().map_or(Step::Owned(Value::None), Step::Borrowed)
}
(Value::Array(arr), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
Step::Owned(Value::Array(
arr.iter()
.map(|v| match v {
Value::Object(obj) => obj.get(name).cloned().unwrap_or(Value::None),
_ => Value::None,
})
.collect::<Vec<_>>()
.into(),
))
}
(Value::Set(set), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
Step::Owned(Value::Set(Set::from(
set.iter()
.map(|v| match v {
Value::Object(obj) => obj.get(name).cloned().unwrap_or(Value::None),
_ => Value::None,
})
.collect::<Vec<_>>(),
)))
}
_ => Step::Owned(Value::None),
}
}
impl fmt::Display for FieldPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, part) in self.0.iter().enumerate() {
match part {
FieldPathPart::Field(name) if i == 0 => write!(f, "{}", name)?,
FieldPathPart::Field(name) => write!(f, ".{}", name)?,
FieldPathPart::Index(idx) => write!(f, "[{}]", idx)?,
FieldPathPart::First => write!(f, "[0]")?,
FieldPathPart::Last => write!(f, "[$]")?,
FieldPathPart::Lookup(key) if i == 0 => write!(f, "{}", key)?,
FieldPathPart::Lookup(key) => write!(f, ".{}", key)?,
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::val::Object;
fn make_obj(pairs: Vec<(&str, Value)>) -> Object {
let map: BTreeMap<String, Value> =
pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
Object::from(map)
}
#[test]
fn test_field_path_simple() {
let path = FieldPath::field("name");
assert_eq!(path.to_string(), "name");
assert_eq!(path.len(), 1);
}
#[test]
fn test_field_path_extract_simple() {
let path = FieldPath::field("name");
let obj = make_obj(vec![("name", Value::from("Alice"))]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::from("Alice"));
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_field_path_extract_nested() {
let path = FieldPath(vec![
FieldPathPart::Field("user".into()),
FieldPathPart::Field("address".into()),
FieldPathPart::Field("city".into()),
]);
let city_obj = make_obj(vec![("city", Value::from("Austin"))]);
let address_obj = make_obj(vec![("address", Value::Object(city_obj))]);
let user_obj = make_obj(vec![("user", Value::Object(address_obj))]);
let value = Value::Object(user_obj);
let result = path.extract(&value);
assert_eq!(*result, Value::from("Austin"));
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_field_path_extract_array_index() {
let path = FieldPath(vec![FieldPathPart::Field("items".into()), FieldPathPart::Index(0)]);
let items = Value::Array(vec![Value::from("first"), Value::from("second")].into());
let obj = make_obj(vec![("items", items)]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::from("first"));
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_field_path_extract_array_last() {
let path = FieldPath(vec![FieldPathPart::Field("items".into()), FieldPathPart::Last]);
let items = Value::Array(vec![Value::from("first"), Value::from("second")].into());
let obj = make_obj(vec![("items", items)]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::from("second"));
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_field_path_extract_missing() {
let path = FieldPath::field("missing");
let obj = make_obj(vec![("name", Value::from("Alice"))]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::None);
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn test_field_path_extract_past_missing_stays_none() {
let path = FieldPath(vec![
FieldPathPart::Field("missing".into()),
FieldPathPart::Field("deeper".into()),
FieldPathPart::Index(3),
]);
let obj = make_obj(vec![("name", Value::from("Alice"))]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::None);
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn test_field_path_extract_field_on_array() {
let path = FieldPath(vec![
FieldPathPart::Field("users".into()),
FieldPathPart::Field("name".into()),
]);
let user1 = Value::Object(make_obj(vec![("name", Value::from("Alice"))]));
let user2 = Value::Object(make_obj(vec![("name", Value::from("Bob"))]));
let users = Value::Array(vec![user1, user2].into());
let obj = make_obj(vec![("users", users)]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert!(matches!(result, Cow::Owned(_)));
if let Value::Array(arr) = result.into_owned() {
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], Value::from("Alice"));
assert_eq!(arr[1], Value::from("Bob"));
} else {
panic!("Expected array result");
}
}
#[test]
fn test_field_path_extract_through_owned_intermediate() {
let path = FieldPath(vec![
FieldPathPart::Field("users".into()),
FieldPathPart::Field("name".into()),
FieldPathPart::Index(0),
]);
let user1 = Value::Object(make_obj(vec![("name", Value::from("Alice"))]));
let user2 = Value::Object(make_obj(vec![("name", Value::from("Bob"))]));
let users = Value::Array(vec![user1, user2].into());
let obj = make_obj(vec![("users", users)]);
let value = Value::Object(obj);
let result = path.extract(&value);
assert_eq!(*result, Value::from("Alice"));
assert!(matches!(result, Cow::Owned(_)));
}
#[test]
fn test_field_path_display() {
let path = FieldPath(vec![
FieldPathPart::Field("user".into()),
FieldPathPart::Field("address".into()),
FieldPathPart::Index(0),
FieldPathPart::Field("city".into()),
]);
assert_eq!(path.to_string(), "user.address[0].city");
}
}