use std::{collections::BTreeMap, fmt};
use hidane_proto::google::firestore::v1::{MapValue, Value, value::ValueType};
pub type Fields = BTreeMap<String, Value>;
#[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);
}
pub fn project(fields: &Fields, paths: &[FieldPath]) -> Fields {
let mut out = Fields::new();
for path in paths {
if let Some(value) = get(fields, path) {
set(&mut out, path, value.clone());
}
}
out
}
pub fn apply_mask(existing: &mut Fields, input: &Fields, mask: &[FieldPath]) {
for path in mask {
match get(input, path) {
Some(value) => set(existing, path, value.clone()),
None => delete(existing, path),
}
}
}
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))])
);
}
#[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))]))]));
}
}