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(&self, value: &Value) -> Value {
let mut current = value.clone();
for part in &self.0 {
current = match (¤t, part) {
(Value::Object(obj), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
obj.get(name).cloned().unwrap_or(Value::None)
}
(Value::Array(arr), FieldPathPart::Index(i)) => {
arr.get(*i).cloned().unwrap_or(Value::None)
}
(Value::Set(set), FieldPathPart::Index(i)) => {
set.nth(*i).cloned().unwrap_or(Value::None)
}
(Value::Array(arr), FieldPathPart::First) => {
arr.first().cloned().unwrap_or(Value::None)
}
(Value::Set(set), FieldPathPart::First) => {
set.first().cloned().unwrap_or(Value::None)
}
(Value::Array(arr), FieldPathPart::Last) => {
arr.last().cloned().unwrap_or(Value::None)
}
(Value::Set(set), FieldPathPart::Last) => {
set.last().cloned().unwrap_or(Value::None)
}
(Value::Array(arr), FieldPathPart::Field(name) | FieldPathPart::Lookup(name)) => {
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)) => {
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<_>>(),
))
}
_ => Value::None,
};
}
current
}
}
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"));
}
#[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"));
}
#[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"));
}
#[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"));
}
#[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);
}
#[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);
if let Value::Array(arr) = result {
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_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");
}
}