use std::fmt;
use hidane_proto::google::firestore::v1::{MapValue, Value, value::ValueType};
pub use hidane_proto::ordered::Fields;
use crate::hash_order::hash_order;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FieldPath(Vec<String>);
impl FieldPath {
pub fn parse(path: &str) -> Result<Self, String> {
let invalid = || {
format!(
"Invalid property path \"{path}\". Unquoted property paths must match regex \
([a-zA-Z_][a-zA-Z_0-9]*), and quoted property paths must match regex \
(`(?:[^`\\\\]|(?:\\\\.))+`)"
)
};
let mut segments = Vec::new();
let mut chars = path.chars().peekable();
loop {
let mut segment = String::new();
if chars.peek() == Some(&'`') {
chars.next();
loop {
match chars.next() {
Some('`') => break,
Some('\\') => segment.push(chars.next().ok_or_else(invalid)?),
Some(c) => segment.push(c),
None => return Err(invalid()),
}
}
if segment.is_empty() {
return Err(invalid());
}
} else {
while let Some(&c) = chars.peek() {
if c == '.' {
break;
}
let valid = if segment.is_empty() {
c.is_ascii_alphabetic() || c == '_'
} else {
c.is_ascii_alphanumeric() || c == '_'
};
if !valid {
return Err(invalid());
}
segment.push(c);
chars.next();
}
if segment.is_empty() {
return Err(invalid());
}
}
segments.push(segment);
match chars.next() {
None => return Ok(Self(segments)),
Some('.') => {}
Some(_) => return Err(invalid()),
}
}
}
pub fn from_segments<I, S>(segments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self(segments.into_iter().map(Into::into).collect())
}
pub fn segments(&self) -> &[String] {
&self.0
}
}
impl fmt::Display for FieldPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, segment) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(".")?;
}
let mut chars = segment.chars();
let simple = chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
if simple {
f.write_str(segment)?;
} else {
f.write_str("`")?;
for c in segment.chars() {
if c == '`' || c == '\\' {
f.write_str("\\")?;
}
write!(f, "{c}")?;
}
f.write_str("`")?;
}
}
Ok(())
}
}
pub fn get<'a>(fields: &'a Fields, path: &FieldPath) -> Option<&'a Value> {
let (last, parents) = path.0.split_last()?;
let mut current = fields;
for segment in parents {
match ¤t.get(segment)?.value_type {
Some(ValueType::MapValue(map)) => current = &map.fields,
_ => return None,
}
}
current.get(last)
}
pub fn set(fields: &mut Fields, path: &FieldPath, value: Value) {
let Some((last, parents)) = path.0.split_last() else {
return;
};
let mut current = fields;
for segment in parents {
let entry = current.entry(segment.clone()).or_insert_with(empty_map);
if !matches!(entry.value_type, Some(ValueType::MapValue(_))) {
*entry = empty_map();
}
let Some(ValueType::MapValue(map)) = &mut entry.value_type else {
unreachable!("just made it a map");
};
current = &mut map.fields;
}
current.insert(last.clone(), value);
}
pub fn delete(fields: &mut Fields, path: &FieldPath) {
let Some((last, parents)) = path.0.split_last() else {
return;
};
let mut current = fields;
for segment in parents {
match current.get_mut(segment).and_then(|v| v.value_type.as_mut()) {
Some(ValueType::MapValue(map)) => current = &mut map.fields,
_ => return,
}
}
current.remove(last);
}
#[derive(Default)]
struct MaskTree {
whole: bool,
children: Vec<(String, MaskTree)>,
}
impl MaskTree {
fn new(paths: &[FieldPath]) -> Self {
let mut root = Self::default();
for path in paths {
let mut node = &mut root;
for segment in path.segments() {
let at = match node.children.iter().position(|(name, _)| name == segment) {
Some(at) => at,
None => {
node.children.push((segment.clone(), Self::default()));
node.children.len() - 1
}
};
node = &mut node.children[at].1;
}
node.whole = true;
}
root
}
fn ordered(&self) -> impl Iterator<Item = &(String, MaskTree)> {
let names: Vec<&str> = self
.children
.iter()
.map(|(name, _)| name.as_str())
.collect();
hash_order(&names).into_iter().map(|i| &self.children[i])
}
}
fn in_hash_order<T>(mut entries: Vec<(String, T)>) -> impl Iterator<Item = (String, T)> {
let order = hash_order(&entries.iter().map(|(name, _)| name).collect::<Vec<_>>());
let mut taken: Vec<Option<(String, T)>> = entries.drain(..).map(Some).collect();
order.into_iter().filter_map(move |i| taken[i].take())
}
fn map_fields(value: &Value) -> Option<&Fields> {
match &value.value_type {
Some(ValueType::MapValue(map)) => Some(&map.fields),
_ => None,
}
}
fn map_of(fields: Fields) -> Value {
Value {
value_type: Some(ValueType::MapValue(MapValue { fields })),
}
}
pub fn project(fields: &Fields, paths: &[FieldPath]) -> Fields {
project_level(fields, &MaskTree::new(paths))
}
fn project_level(fields: &Fields, mask: &MaskTree) -> Fields {
let mut found = Vec::new();
for (name, below) in mask.ordered() {
let Some(value) = fields.get(name) else {
continue;
};
if below.whole {
found.push((name.clone(), value.clone()));
} else if let Some(map) = map_fields(value) {
let inner = project_level(map, below);
if !inner.is_empty() {
found.push((name.clone(), map_of(inner)));
}
}
}
in_hash_order(found).collect()
}
pub fn apply_mask(existing: &mut Fields, input: &Fields, mask: &[FieldPath]) {
merge_level(existing, Some(input), &MaskTree::new(mask));
}
fn merge_level(existing: &mut Fields, input: Option<&Fields>, mask: &MaskTree) {
let mut changes = Vec::new();
for (name, below) in mask.ordered() {
let old = existing.get(name);
let given = input.and_then(|fields| fields.get(name));
let new = if below.whole {
given.cloned()
} else {
let mut inner = old.and_then(map_fields).cloned().unwrap_or_default();
merge_level(&mut inner, given.and_then(map_fields), below);
match old {
Some(old) if map_fields(old).is_some() || !inner.is_empty() => Some(map_of(inner)),
Some(old) => Some(old.clone()),
None if inner.is_empty() => None,
None => Some(map_of(inner)),
}
};
if new.as_ref() != old {
changes.push((name.clone(), new));
}
}
for (name, new) in in_hash_order(changes) {
existing.remove(&name);
if let Some(value) = new {
existing.insert(name, value);
}
}
}
fn empty_map() -> Value {
Value {
value_type: Some(ValueType::MapValue(MapValue::default())),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn int(i: i64) -> Value {
Value {
value_type: Some(ValueType::IntegerValue(i)),
}
}
fn map(entries: &[(&str, Value)]) -> Value {
Value {
value_type: Some(ValueType::MapValue(MapValue {
fields: entries
.iter()
.map(|(k, v)| ((*k).to_owned(), v.clone()))
.collect(),
})),
}
}
fn fields(entries: &[(&str, Value)]) -> Fields {
entries
.iter()
.map(|(k, v)| ((*k).to_owned(), v.clone()))
.collect()
}
fn fp(s: &str) -> FieldPath {
FieldPath::parse(s).unwrap()
}
#[test]
fn display_is_the_canonical_form() {
for canonical in [
"a", "a.b", "_x9", "`a b`.c", "`1a`", "`x\\`y`", "`a\\\\b`", "`é`",
] {
assert_eq!(fp(canonical).to_string(), canonical);
}
assert_eq!(fp("`a`.`b`").to_string(), "a.b");
}
#[test]
fn parses_unquoted_and_quoted_segments() {
assert_eq!(fp("a").segments(), ["a"]);
assert_eq!(fp("a.b_1._c").segments(), ["a", "b_1", "_c"]);
assert_eq!(fp("`a.b`.c").segments(), ["a.b", "c"]);
assert_eq!(fp("`x\\`y`").segments(), ["x`y"]);
assert_eq!(fp("`\\\\`").segments(), ["\\"]);
assert_eq!(fp("`1st`").segments(), ["1st"]);
}
#[test]
fn rejects_like_the_official_emulator() {
for bad in [
"", "a..b", ".a", "a.", "1a", "a-b", "``", "`a", "`a`b", "a b",
] {
let err = FieldPath::parse(bad).unwrap_err();
assert!(
err.starts_with(&format!("Invalid property path \"{bad}\". Unquoted")),
"{bad}: {err}"
);
}
assert_eq!(
FieldPath::parse("a..b").unwrap_err(),
"Invalid property path \"a..b\". Unquoted property paths must match regex \
([a-zA-Z_][a-zA-Z_0-9]*), and quoted property paths must match regex \
(`(?:[^`\\\\]|(?:\\\\.))+`)"
);
}
#[test]
fn update_mask_matches_the_official_emulator() {
let mut doc = fields(&[
("a", int(1)),
("b", int(2)),
("c", map(&[("x", int(1)), ("y", int(2))])),
]);
let input = fields(&[("a", int(10)), ("c", map(&[("x", int(5))])), ("e", int(9))]);
apply_mask(&mut doc, &input, &[fp("a"), fp("c.x"), fp("d")]);
assert_eq!(
doc,
fields(&[
("a", int(10)),
("b", int(2)),
("c", map(&[("x", int(5)), ("y", int(2))])),
])
);
}
#[test]
fn masks_on_missing_documents_create_only_the_masked_fields() {
let mut doc = Fields::new();
let input = fields(&[("a", int(1)), ("b", map(&[("x", int(1))]))]);
apply_mask(&mut doc, &input, &[fp("b.x")]);
assert_eq!(doc, fields(&[("b", map(&[("x", int(1))]))]));
}
#[test]
fn read_masks_project_nested_fields() {
let doc = fields(&[
("a", int(1)),
("b", map(&[("x", int(1)), ("y", int(2))])),
("c", int(3)),
]);
assert_eq!(
project(
&doc,
&[fp("b.x"), fp("c"), fp("missing"), fp("a.not_a_map")]
),
fields(&[("b", map(&[("x", int(1))])), ("c", int(3))])
);
}
fn layout(fields: &Fields) -> String {
fields
.iter()
.map(|(name, value)| match &value.value_type {
Some(ValueType::MapValue(map)) => format!("{name}{{{}}}", layout(&map.fields)),
_ => name.clone(),
})
.collect::<Vec<_>>()
.join(",")
}
#[test]
fn masked_fields_that_change_move_to_the_end_in_hash_order() {
let base = || {
fields(&[
("zeta", int(1)),
("alpha", int(2)),
("mid", map(&[("z", int(3)), ("a", int(4))])),
])
};
let mut doc = base();
apply_mask(
&mut doc,
&fields(&[("beta", int(9)), ("alpha", int(10))]),
&[fp("beta"), fp("alpha")],
);
assert_eq!(layout(&doc), "zeta,mid{z,a},alpha,beta");
let mut doc = base();
apply_mask(
&mut doc,
&fields(&[
("deep", map(&[("x", map(&[("y", int(1))]))])),
("alpha", int(2)),
]),
&[fp("deep.x.y"), fp("alpha")],
);
assert_eq!(layout(&doc), "zeta,alpha,mid{z,a},deep{x{y}}");
}
#[test]
fn a_map_changed_inside_moves_to_the_end() {
let base = || {
fields(&[
("mid", map(&[("z", int(3)), ("a", int(4))])),
("zeta", int(1)),
("alpha", int(2)),
])
};
let mut doc = base();
apply_mask(
&mut doc,
&fields(&[("mid", map(&[("b", int(5))]))]),
&[fp("mid.b")],
);
assert_eq!(layout(&doc), "zeta,alpha,mid{z,a,b}");
let mut doc = base();
apply_mask(&mut doc, &Fields::new(), &[fp("mid.z")]);
assert_eq!(layout(&doc), "zeta,alpha,mid{a}");
let mut doc = fields(&[
("p", map(&[("x", int(1)), ("y", int(2))])),
("alpha", int(1)),
("zeta", int(2)),
("mid", int(3)),
]);
apply_mask(
&mut doc,
&fields(&[("alpha", int(9))]),
&[fp("p.y"), fp("alpha")],
);
assert_eq!(layout(&doc), "zeta,mid,p{x},alpha");
}
#[test]
fn the_changed_fields_have_a_hash_map_of_their_own() {
let present = [
"str", "int", "dbl", "zeta", "alpha", "a", "b", "c", "x1", "x2", "x3",
];
let doc_with = |extra: &[(&str, Value)]| {
let mut doc = fields(&[("keep", int(0))]);
for (n, name) in present.iter().enumerate() {
doc.insert((*name).to_owned(), int(n as i64));
}
doc.extend(extra.iter().map(|(k, v)| ((*k).to_owned(), v.clone())));
doc
};
let input: Fields = present
.iter()
.enumerate()
.map(|(n, name)| ((*name).to_owned(), int(n as i64 + 100)))
.collect();
let mask: Vec<FieldPath> = present
.iter()
.chain(&["gone1", "gone2"])
.map(|p| fp(p))
.collect();
let mut doc = doc_with(&[]);
apply_mask(&mut doc, &input, &mask);
assert_eq!(layout(&doc), "keep,zeta,str,a,b,c,alpha,x1,x2,x3,int,dbl");
let mut doc = doc_with(&[("gone1", int(1)), ("gone2", int(2))]);
apply_mask(&mut doc, &input, &mask);
assert_eq!(layout(&doc), "keep,zeta,a,b,c,int,dbl,str,alpha,x1,x2,x3");
}
#[test]
fn read_masks_return_fields_in_hash_order() {
let doc = fields(&[
("str", int(1)),
("mid", map(&[("z", int(1)), ("a", int(2))])),
("zeta", int(3)),
("alpha", int(4)),
("a", int(5)),
]);
let masked = |mask: &[&str]| {
layout(&project(
&doc,
&mask.iter().map(|p| fp(p)).collect::<Vec<_>>(),
))
};
assert_eq!(masked(&["a", "alpha", "zeta"]), "zeta,a,alpha");
assert_eq!(masked(&["mid.a", "mid.z", "str"]), "str,mid{a,z}");
assert_eq!(masked(&["mid", "str"]), "str,mid{z,a}");
}
#[test]
fn set_replaces_non_map_intermediates() {
let mut doc = fields(&[("a", int(1))]);
set(&mut doc, &fp("a.b"), int(2));
assert_eq!(doc, fields(&[("a", map(&[("b", int(2))]))]));
}
}