Skip to main content

hidane_core/
field_path.rs

1//! Field paths (`a.b`, `` `x.y`.z ``) and the operations masks need on document fields.
2//!
3//! Syntax, as the official emulator enforces it: segments are separated by `.`; an unquoted
4//! segment matches `[a-zA-Z_][a-zA-Z_0-9]*`; any other segment is wrapped in backticks, where
5//! `` \` `` and `\\` are escapes.
6
7use std::fmt;
8
9use hidane_proto::google::firestore::v1::{MapValue, Value, value::ValueType};
10pub use hidane_proto::ordered::Fields;
11
12use crate::hash_order::hash_order;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct FieldPath(Vec<String>);
16
17impl FieldPath {
18    /// Parses a field path; the error is the official emulator's message.
19    pub fn parse(path: &str) -> Result<Self, String> {
20        let invalid = || {
21            format!(
22                "Invalid property path \"{path}\". Unquoted property paths must match regex \
23                 ([a-zA-Z_][a-zA-Z_0-9]*), and quoted property paths must match regex \
24                 (`(?:[^`\\\\]|(?:\\\\.))+`)"
25            )
26        };
27        let mut segments = Vec::new();
28        let mut chars = path.chars().peekable();
29        loop {
30            let mut segment = String::new();
31            if chars.peek() == Some(&'`') {
32                chars.next();
33                loop {
34                    match chars.next() {
35                        Some('`') => break,
36                        Some('\\') => segment.push(chars.next().ok_or_else(invalid)?),
37                        Some(c) => segment.push(c),
38                        None => return Err(invalid()),
39                    }
40                }
41                if segment.is_empty() {
42                    return Err(invalid());
43                }
44            } else {
45                while let Some(&c) = chars.peek() {
46                    if c == '.' {
47                        break;
48                    }
49                    let valid = if segment.is_empty() {
50                        c.is_ascii_alphabetic() || c == '_'
51                    } else {
52                        c.is_ascii_alphanumeric() || c == '_'
53                    };
54                    if !valid {
55                        return Err(invalid());
56                    }
57                    segment.push(c);
58                    chars.next();
59                }
60                if segment.is_empty() {
61                    return Err(invalid());
62                }
63            }
64            segments.push(segment);
65            match chars.next() {
66                None => return Ok(Self(segments)),
67                Some('.') => {}
68                Some(_) => return Err(invalid()),
69            }
70        }
71    }
72
73    pub fn from_segments<I, S>(segments: I) -> Self
74    where
75        I: IntoIterator<Item = S>,
76        S: Into<String>,
77    {
78        Self(segments.into_iter().map(Into::into).collect())
79    }
80
81    pub fn segments(&self) -> &[String] {
82        &self.0
83    }
84}
85
86/// The canonical form: simple segments as they are, others in backticks with `` ` `` and `\`
87/// escaped, joined by `.`.
88impl fmt::Display for FieldPath {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        for (i, segment) in self.0.iter().enumerate() {
91            if i > 0 {
92                f.write_str(".")?;
93            }
94            let mut chars = segment.chars();
95            let simple = chars
96                .next()
97                .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
98                && chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
99            if simple {
100                f.write_str(segment)?;
101            } else {
102                f.write_str("`")?;
103                for c in segment.chars() {
104                    if c == '`' || c == '\\' {
105                        f.write_str("\\")?;
106                    }
107                    write!(f, "{c}")?;
108                }
109                f.write_str("`")?;
110            }
111        }
112        Ok(())
113    }
114}
115
116/// The value at `path`, if every segment but the last names a map.
117pub fn get<'a>(fields: &'a Fields, path: &FieldPath) -> Option<&'a Value> {
118    let (last, parents) = path.0.split_last()?;
119    let mut current = fields;
120    for segment in parents {
121        match &current.get(segment)?.value_type {
122            Some(ValueType::MapValue(map)) => current = &map.fields,
123            _ => return None,
124        }
125    }
126    current.get(last)
127}
128
129/// Sets `path` to `value`, creating (or replacing non-map values with) intermediate maps.
130pub fn set(fields: &mut Fields, path: &FieldPath, value: Value) {
131    let Some((last, parents)) = path.0.split_last() else {
132        return;
133    };
134    let mut current = fields;
135    for segment in parents {
136        let entry = current.entry(segment.clone()).or_insert_with(empty_map);
137        if !matches!(entry.value_type, Some(ValueType::MapValue(_))) {
138            *entry = empty_map();
139        }
140        let Some(ValueType::MapValue(map)) = &mut entry.value_type else {
141            unreachable!("just made it a map");
142        };
143        current = &mut map.fields;
144    }
145    current.insert(last.clone(), value);
146}
147
148/// Removes `path` if it exists. Intermediate maps are kept even if they become empty.
149pub fn delete(fields: &mut Fields, path: &FieldPath) {
150    let Some((last, parents)) = path.0.split_last() else {
151        return;
152    };
153    let mut current = fields;
154    for segment in parents {
155        match current.get_mut(segment).and_then(|v| v.value_type.as_mut()) {
156            Some(ValueType::MapValue(map)) => current = &mut map.fields,
157            _ => return,
158        }
159    }
160    current.remove(last);
161}
162
163/// Masked paths as a tree of segments. Each level lists its names in the order they first
164/// appear in the mask.
165#[derive(Default)]
166struct MaskTree {
167    /// The mask names this exact path: its whole value is taken.
168    whole: bool,
169    children: Vec<(String, MaskTree)>,
170}
171
172impl MaskTree {
173    fn new(paths: &[FieldPath]) -> Self {
174        let mut root = Self::default();
175        for path in paths {
176            let mut node = &mut root;
177            for segment in path.segments() {
178                let at = match node.children.iter().position(|(name, _)| name == segment) {
179                    Some(at) => at,
180                    None => {
181                        node.children.push((segment.clone(), Self::default()));
182                        node.children.len() - 1
183                    }
184                };
185                node = &mut node.children[at].1;
186            }
187            node.whole = true;
188        }
189        root
190    }
191
192    /// The names of this level in the order the official emulator visits them: the order of
193    /// a Java `HashMap` that received them in mask order.
194    fn ordered(&self) -> impl Iterator<Item = &(String, MaskTree)> {
195        let names: Vec<&str> = self
196            .children
197            .iter()
198            .map(|(name, _)| name.as_str())
199            .collect();
200        hash_order(&names).into_iter().map(|i| &self.children[i])
201    }
202}
203
204/// `entries` in the order of a Java `HashMap` that received them in turn.
205fn in_hash_order<T>(mut entries: Vec<(String, T)>) -> impl Iterator<Item = (String, T)> {
206    let order = hash_order(&entries.iter().map(|(name, _)| name).collect::<Vec<_>>());
207    let mut taken: Vec<Option<(String, T)>> = entries.drain(..).map(Some).collect();
208    order.into_iter().filter_map(move |i| taken[i].take())
209}
210
211fn map_fields(value: &Value) -> Option<&Fields> {
212    match &value.value_type {
213        Some(ValueType::MapValue(map)) => Some(&map.fields),
214        _ => None,
215    }
216}
217
218fn map_of(fields: Fields) -> Value {
219    Value {
220        value_type: Some(ValueType::MapValue(MapValue { fields })),
221    }
222}
223
224/// Keeps only the fields named by `paths` (a read mask). As on the official emulator, each map
225/// lists what it returns in the order of a Java `HashMap`: the masked names are visited in the
226/// order of one, and the values found are put into another (#108).
227pub fn project(fields: &Fields, paths: &[FieldPath]) -> Fields {
228    project_level(fields, &MaskTree::new(paths))
229}
230
231fn project_level(fields: &Fields, mask: &MaskTree) -> Fields {
232    let mut found = Vec::new();
233    for (name, below) in mask.ordered() {
234        let Some(value) = fields.get(name) else {
235            continue;
236        };
237        if below.whole {
238            found.push((name.clone(), value.clone()));
239        } else if let Some(map) = map_fields(value) {
240            let inner = project_level(map, below);
241            if !inner.is_empty() {
242                found.push((name.clone(), map_of(inner)));
243            }
244        }
245    }
246    in_hash_order(found).collect()
247}
248
249/// Applies an update mask: every masked path takes its value from `input`, or is deleted when
250/// `input` has no value there. Fields outside the mask are untouched, including fields of
251/// `input` that the mask does not name.
252///
253/// Where fields land follows the official emulator (#108): at each level, the masked names are
254/// visited in the order of a Java `HashMap`, those whose value changes are put into another
255/// one, and in that map's order each is removed and, unless deleted, added again at the end.
256/// A map changed inside moves the same way; fields that keep their value stay where they are.
257pub fn apply_mask(existing: &mut Fields, input: &Fields, mask: &[FieldPath]) {
258    merge_level(existing, Some(input), &MaskTree::new(mask));
259}
260
261fn merge_level(existing: &mut Fields, input: Option<&Fields>, mask: &MaskTree) {
262    let mut changes = Vec::new();
263    for (name, below) in mask.ordered() {
264        let old = existing.get(name);
265        let given = input.and_then(|fields| fields.get(name));
266        let new = if below.whole {
267            given.cloned()
268        } else {
269            let mut inner = old.and_then(map_fields).cloned().unwrap_or_default();
270            merge_level(&mut inner, given.and_then(map_fields), below);
271            match old {
272                // A map, or something the masked paths below turn into one.
273                Some(old) if map_fields(old).is_some() || !inner.is_empty() => Some(map_of(inner)),
274                Some(old) => Some(old.clone()),
275                None if inner.is_empty() => None,
276                None => Some(map_of(inner)),
277            }
278        };
279        if new.as_ref() != old {
280            changes.push((name.clone(), new));
281        }
282    }
283    for (name, new) in in_hash_order(changes) {
284        existing.remove(&name);
285        if let Some(value) = new {
286            existing.insert(name, value);
287        }
288    }
289}
290
291fn empty_map() -> Value {
292    Value {
293        value_type: Some(ValueType::MapValue(MapValue::default())),
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    fn int(i: i64) -> Value {
302        Value {
303            value_type: Some(ValueType::IntegerValue(i)),
304        }
305    }
306
307    fn map(entries: &[(&str, Value)]) -> Value {
308        Value {
309            value_type: Some(ValueType::MapValue(MapValue {
310                fields: entries
311                    .iter()
312                    .map(|(k, v)| ((*k).to_owned(), v.clone()))
313                    .collect(),
314            })),
315        }
316    }
317
318    fn fields(entries: &[(&str, Value)]) -> Fields {
319        entries
320            .iter()
321            .map(|(k, v)| ((*k).to_owned(), v.clone()))
322            .collect()
323    }
324
325    fn fp(s: &str) -> FieldPath {
326        FieldPath::parse(s).unwrap()
327    }
328
329    #[test]
330    fn display_is_the_canonical_form() {
331        for canonical in [
332            "a", "a.b", "_x9", "`a b`.c", "`1a`", "`x\\`y`", "`a\\\\b`", "`é`",
333        ] {
334            assert_eq!(fp(canonical).to_string(), canonical);
335        }
336        assert_eq!(fp("`a`.`b`").to_string(), "a.b");
337    }
338
339    #[test]
340    fn parses_unquoted_and_quoted_segments() {
341        assert_eq!(fp("a").segments(), ["a"]);
342        assert_eq!(fp("a.b_1._c").segments(), ["a", "b_1", "_c"]);
343        assert_eq!(fp("`a.b`.c").segments(), ["a.b", "c"]);
344        assert_eq!(fp("`x\\`y`").segments(), ["x`y"]);
345        assert_eq!(fp("`\\\\`").segments(), ["\\"]);
346        assert_eq!(fp("`1st`").segments(), ["1st"]);
347    }
348
349    #[test]
350    fn rejects_like_the_official_emulator() {
351        for bad in [
352            "", "a..b", ".a", "a.", "1a", "a-b", "``", "`a", "`a`b", "a b",
353        ] {
354            let err = FieldPath::parse(bad).unwrap_err();
355            assert!(
356                err.starts_with(&format!("Invalid property path \"{bad}\". Unquoted")),
357                "{bad}: {err}"
358            );
359        }
360        // The exact text the official emulator returns (tests/fixtures/document_writes.json).
361        assert_eq!(
362            FieldPath::parse("a..b").unwrap_err(),
363            "Invalid property path \"a..b\". Unquoted property paths must match regex \
364             ([a-zA-Z_][a-zA-Z_0-9]*), and quoted property paths must match regex \
365             (`(?:[^`\\\\]|(?:\\\\.))+`)"
366        );
367    }
368
369    #[test]
370    fn update_mask_matches_the_official_emulator() {
371        // Observed: {a:1, b:2, c:{x:1, y:2}} patched with mask [a, c.x, d] and input
372        // {a:10, c:{x:5}, e:9} becomes {a:10, b:2, c:{x:5, y:2}}.
373        let mut doc = fields(&[
374            ("a", int(1)),
375            ("b", int(2)),
376            ("c", map(&[("x", int(1)), ("y", int(2))])),
377        ]);
378        let input = fields(&[("a", int(10)), ("c", map(&[("x", int(5))])), ("e", int(9))]);
379        apply_mask(&mut doc, &input, &[fp("a"), fp("c.x"), fp("d")]);
380        assert_eq!(
381            doc,
382            fields(&[
383                ("a", int(10)),
384                ("b", int(2)),
385                ("c", map(&[("x", int(5)), ("y", int(2))])),
386            ])
387        );
388    }
389
390    #[test]
391    fn masks_on_missing_documents_create_only_the_masked_fields() {
392        let mut doc = Fields::new();
393        let input = fields(&[("a", int(1)), ("b", map(&[("x", int(1))]))]);
394        apply_mask(&mut doc, &input, &[fp("b.x")]);
395        assert_eq!(doc, fields(&[("b", map(&[("x", int(1))]))]));
396    }
397
398    #[test]
399    fn read_masks_project_nested_fields() {
400        let doc = fields(&[
401            ("a", int(1)),
402            ("b", map(&[("x", int(1)), ("y", int(2))])),
403            ("c", int(3)),
404        ]);
405        assert_eq!(
406            project(
407                &doc,
408                &[fp("b.x"), fp("c"), fp("missing"), fp("a.not_a_map")]
409            ),
410            fields(&[("b", map(&[("x", int(1))])), ("c", int(3))])
411        );
412    }
413
414    /// The names of `fields`, nested maps as `name{...}`, in the order they are held.
415    fn layout(fields: &Fields) -> String {
416        fields
417            .iter()
418            .map(|(name, value)| match &value.value_type {
419                Some(ValueType::MapValue(map)) => format!("{name}{{{}}}", layout(&map.fields)),
420                _ => name.clone(),
421            })
422            .collect::<Vec<_>>()
423            .join(",")
424    }
425
426    // The cases below are recorded from the official emulator in
427    // crates/hidane/tests/fixtures/field_order.json (#108).
428
429    #[test]
430    fn masked_fields_that_change_move_to_the_end_in_hash_order() {
431        let base = || {
432            fields(&[
433                ("zeta", int(1)),
434                ("alpha", int(2)),
435                ("mid", map(&[("z", int(3)), ("a", int(4))])),
436            ])
437        };
438        let mut doc = base();
439        apply_mask(
440            &mut doc,
441            &fields(&[("beta", int(9)), ("alpha", int(10))]),
442            &[fp("beta"), fp("alpha")],
443        );
444        assert_eq!(layout(&doc), "zeta,mid{z,a},alpha,beta");
445
446        // alpha keeps its value, so it stays.
447        let mut doc = base();
448        apply_mask(
449            &mut doc,
450            &fields(&[
451                ("deep", map(&[("x", map(&[("y", int(1))]))])),
452                ("alpha", int(2)),
453            ]),
454            &[fp("deep.x.y"), fp("alpha")],
455        );
456        assert_eq!(layout(&doc), "zeta,alpha,mid{z,a},deep{x{y}}");
457    }
458
459    #[test]
460    fn a_map_changed_inside_moves_to_the_end() {
461        let base = || {
462            fields(&[
463                ("mid", map(&[("z", int(3)), ("a", int(4))])),
464                ("zeta", int(1)),
465                ("alpha", int(2)),
466            ])
467        };
468        let mut doc = base();
469        apply_mask(
470            &mut doc,
471            &fields(&[("mid", map(&[("b", int(5))]))]),
472            &[fp("mid.b")],
473        );
474        assert_eq!(layout(&doc), "zeta,alpha,mid{z,a,b}");
475
476        let mut doc = base();
477        apply_mask(&mut doc, &Fields::new(), &[fp("mid.z")]);
478        assert_eq!(layout(&doc), "zeta,alpha,mid{a}");
479
480        let mut doc = fields(&[
481            ("p", map(&[("x", int(1)), ("y", int(2))])),
482            ("alpha", int(1)),
483            ("zeta", int(2)),
484            ("mid", int(3)),
485        ]);
486        apply_mask(
487            &mut doc,
488            &fields(&[("alpha", int(9))]),
489            &[fp("p.y"), fp("alpha")],
490        );
491        assert_eq!(layout(&doc), "zeta,mid,p{x},alpha");
492    }
493
494    #[test]
495    fn the_changed_fields_have_a_hash_map_of_their_own() {
496        let present = [
497            "str", "int", "dbl", "zeta", "alpha", "a", "b", "c", "x1", "x2", "x3",
498        ];
499        let doc_with = |extra: &[(&str, Value)]| {
500            let mut doc = fields(&[("keep", int(0))]);
501            for (n, name) in present.iter().enumerate() {
502                doc.insert((*name).to_owned(), int(n as i64));
503            }
504            doc.extend(extra.iter().map(|(k, v)| ((*k).to_owned(), v.clone())));
505            doc
506        };
507        let input: Fields = present
508            .iter()
509            .enumerate()
510            .map(|(n, name)| ((*name).to_owned(), int(n as i64 + 100)))
511            .collect();
512        let mask: Vec<FieldPath> = present
513            .iter()
514            .chain(&["gone1", "gone2"])
515            .map(|p| fp(p))
516            .collect();
517
518        // Thirteen masked names, eleven changes: the changes fit 16 buckets.
519        let mut doc = doc_with(&[]);
520        apply_mask(&mut doc, &input, &mask);
521        assert_eq!(layout(&doc), "keep,zeta,str,a,b,c,alpha,x1,x2,x3,int,dbl");
522
523        // Deleting two fields makes thirteen changes: 32 buckets.
524        let mut doc = doc_with(&[("gone1", int(1)), ("gone2", int(2))]);
525        apply_mask(&mut doc, &input, &mask);
526        assert_eq!(layout(&doc), "keep,zeta,a,b,c,int,dbl,str,alpha,x1,x2,x3");
527    }
528
529    #[test]
530    fn read_masks_return_fields_in_hash_order() {
531        let doc = fields(&[
532            ("str", int(1)),
533            ("mid", map(&[("z", int(1)), ("a", int(2))])),
534            ("zeta", int(3)),
535            ("alpha", int(4)),
536            ("a", int(5)),
537        ]);
538        let masked = |mask: &[&str]| {
539            layout(&project(
540                &doc,
541                &mask.iter().map(|p| fp(p)).collect::<Vec<_>>(),
542            ))
543        };
544        assert_eq!(masked(&["a", "alpha", "zeta"]), "zeta,a,alpha");
545        assert_eq!(masked(&["mid.a", "mid.z", "str"]), "str,mid{a,z}");
546        // A masked map keeps its own order.
547        assert_eq!(masked(&["mid", "str"]), "str,mid{z,a}");
548    }
549
550    #[test]
551    fn set_replaces_non_map_intermediates() {
552        let mut doc = fields(&[("a", int(1))]);
553        set(&mut doc, &fp("a.b"), int(2));
554        assert_eq!(doc, fields(&[("a", map(&[("b", int(2))]))]));
555    }
556}